Logged — Accumulated Logs
266 B minified + gzipped
In application development, collecting audit trails or diagnostic traces across multi-step computations is a familiar requirement: pricing pipelines, tax calculations, compliance checks, and data migrations often need to explain how intermediate decisions were reached.
To keep functions pure without mutable globals, log messages are typically threaded manually alongside results using tuples:
This pattern is straightforward and familiar, but as workflows grow, manual log threading requires repetitive plumbing: callers must unpack intermediate tuples, allocate arrays, and explicitly push logs at each stage. If a push call is omitted, intermediate logs are lost without compiler feedback. Alternatively, calling global loggers directly inside business logic couples pure calculations to external I/O, complicates isolated unit testing, and interleaves traces during concurrent runs.
Logged<L, A> simplifies this workflow. It pairs a value A with an immutable sequence of log
entries L, automatically concatenating logs in chronological order as transformations are chained.
Type Structure & Mental Model
Section titled “Type Structure & Mental Model”Logged<L, A> represents a value A paired with an immutable read-only sequence of log entries
L:
flowchart TD
subgraph Step1["Step 1: Base Value"]
A["Input: Base Price (100)"] --> B["Logged.make(100, ['Cart initialized'])"]
end
subgraph Step2["Step 2: Logged.chain(applyDiscount)"]
B --> C["Evaluates intermediate value (100)"]
C --> D["Step Logged: Logged.make(90, ['Member discount: Applied'])"]
end
subgraph Step3["Step 3: Automatic Concatenation"]
D --> E["Combined: { value: 90, log: ['Cart initialized', 'Member discount: Applied'] }"]
end
Step1 ~~~ Step2
Step2 ~~~ Step3
Logs are treated strictly as immutable data. As operations sequence through a pipeline, log entries accumulate automatically in chronological FIFO order without mutable arrays or parameter threading.
Creating Logged Values
Section titled “Creating Logged Values”Lift raw values into Logged using Logged.make, or record a standalone log event using
Logged.from.entry:
Logged.make(value, log?) wraps an existing payload and defaults the log array to [].
Logged.from.entry(entry) writes a single log entry and sets value to undefined, ready to be
sequenced into a pipeline with chain.
Transforming Values and Logs
Section titled “Transforming Values and Logs”Logged provides distinct combinators to transform the payload channel, modify the log channel, or
sequence dependent operations.
Transforming values with map
Section titled “Transforming values with map”map transforms the contained value while leaving the accumulated log completely untouched:
Transforming logs with mapLog
Section titled “Transforming logs with mapLog”mapLog applies a mapping function to every accumulated log entry without altering the contained
value. Use this to attach timestamps, prepend subsystem tags, or redact sensitive credentials before
external export:
Sequencing operations with chain
Section titled “Sequencing operations with chain”chain sequences two computations where the second operation depends on the output value of the
first. The log entries from both steps are automatically concatenated in execution order:
Combining independent logged values with apply
Section titled “Combining independent logged values with apply”When combining multiple independent Logged computations, Logged.apply applies a wrapped curried
function to a wrapped argument, concatenating logs from both sides in order:
Side-Effects and Telemetry
Section titled “Side-Effects and Telemetry”Use tap and tapLog to execute side effects during pipeline execution without altering container
contents or log traces.
Inspecting values with tap
Section titled “Inspecting values with tap”Logged.tap executes a callback receiving the contained payload:
Inspecting logs with tapLog
Section titled “Inspecting logs with tapLog”Logged.tapLog executes a callback receiving the complete read-only array of accumulated logs:
Object Accumulation: assignTo and assign
Section titled “Object Accumulation: assignTo and assign”When accumulating multiple sequential computations into a single object, nesting chain calls
produces deep indentation. Use assignTo and assign to construct an accumulator record
step-by-step:
Optics & Subfields: focus
Section titled “Optics & Subfields: focus”Logged.focus pairs with a Lens to run a logged computation on a nested property within a larger
record, updating the focused field while accumulating any log entries produced during that
sub-computation:
Extracting Results with to.Pair
Section titled “Extracting Results with to.Pair”Extract the payload and accumulated log entries at the boundary of your program using
Logged.to.Pair:
Logged.to.Pair returns a standard Pair<A, ReadonlyArray<L>> (a readonly [A, ReadonlyArray<L>]
tuple). Because Logged is an eager data container, conversion to a tuple avoids misleading
execution terminology.
Practical Example: A Business Rules Engine
Section titled “Practical Example: A Business Rules Engine”Consider an e-commerce checkout engine where multiple promotional discount rules evaluate a cart. Each rule must record whether it was applied or why it was skipped, creating a complete audit trail for customer support:
Each promotional rule remains completely modular. Rules declare their own log messages in isolation,
and chain stitches the audit trail together as computations run.
Problems it solves
Section titled “Problems it solves”- Auditable business calculations: In financial settlement, pricing pipelines, and tax
estimation, each computation must output both the final number and an immutable audit trail
explaining each deduction or adjustment.
Loggedcollects these audit records without mutable global state. - Diagnostic warning accumulation: When parsing external data or migrating legacy schemas, non-fatal warnings (such as deprecated field usage or default fallbacks applied) must travel alongside the transformed payload without interrupting pipeline execution.
- Mock-free log verification: Testing logging behavior traditionally requires intercepting
stdoutor mocking logger instances.Loggedtreats logs as standard return data, allowing unit test suites to assert directly against log arrays using plaintoStrictEqual().