Lazy — Memoized Computations
When developing software, we frequently face a design trade-off between eager evaluation and repeated execution.
Consider a heavy synchronous operation, such as parsing a large local configuration payload or compiling a complex regular expression sheet. If we evaluate it eagerly at startup, we pay the computational cost immediately, even if the specific code path that requires the configuration is never executed.
If we attempt to defer it by wrapping it in a standard function thunk () => A, we solve the
startup problem, but we introduce a new friction: the operation is executed and re-computed on
every single call, wasting CPU cycles over and over.
Lazy<A> represents the elegant middle ground. It is a simple data structure that wraps a
synchronous computation:
Lazy defers the execution of the computation until the exact moment the value is first requested.
Once evaluated, it caches the result, serving it instantly from memory for all subsequent requests
without ever executing the underlying operation again.
Creating and Evaluating
Section titled “Creating and Evaluating”We lift synchronous thunks into the Lazy context using its core constructor:
To force the evaluation of the thunk and extract the cached result, we use Lazy.evaluate:
Transforming and Sequencing
Section titled “Transforming and Sequencing”You can map over and sequence lazy computations point-free without triggering their evaluation.
Transforming results with map
Section titled “Transforming results with map”map describes how the deferred value should be transformed once it is eventually requested,
returning a new Lazy container:
Sequencing dependencies with chain
Section titled “Sequencing dependencies with chain”When a transformation itself returns a Lazy container, we use chain to flatten the nested
context:
Peeking with tap
Section titled “Peeking with tap”Lazy.tap executes a side-effectful callback when the lazy container is evaluated for the first
time, passing the computed value through unchanged:
When to use Lazy
Section titled “When to use Lazy”Use Lazy when:
Section titled “Use Lazy when:”- The operation is expensive and optional: You have a synchronous calculation (like reading schema specs or parsing configs) that is only required in specific execution branches.
- You require single-run memoization: You want a computation to run at most once per request or application lifecycle, caching the outcome for all subsequent steps.
Keep using other types when:
Section titled “Keep using other types when:”- The task is asynchronous: Never use
Lazyfor network or database tasks — useTaskinstead. - The side effect must repeat: If an operation needs to execute a side effect on every single
call (such as returning a fresh timestamp or generating a new random index), use a standard
function thunk
() => A. - The value is always required immediately: If a value is guaranteed to be consumed instantly at startup, evaluate it directly rather than wrapping it in the lazy container.