Skip to content

Lazy — Memoized Computations

Evaluating expensive synchronous operations (such as large config parsing or regex compilation) eagerly at startup increases initialization time, while plain thunks (() => A) recompute on every call.

Lazy<A> defers execution until first accessed via .get() and caches the result for all subsequent evaluations:

type Lazy<A> = {
  readonly get: () => A;
};

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.


We lift synchronous thunks into the Lazy context using its core constructor:

import { Lazy } from "@nlozgachev/pipelined/core";

// The computation is defined, but nothing runs yet
const config = Lazy.from(() => parseExpensiveConfiguration(rawInput));

To force the evaluation of the thunk and extract the cached result, we use Lazy.evaluate:

// 1. First read: the expensive parser executes
const value1 = Lazy.evaluate(config);

// 2. Second read: returns the cached value instantly
const value2 = Lazy.evaluate(config);

You can map over and sequence lazy computations point-free without triggering their evaluation.

map describes how the deferred value should be transformed once it is eventually requested, returning a new Lazy container:

import { pipe } from "@nlozgachev/pipelined/composition";

const databaseUrl = pipe(
  Lazy.from(() => loadConfiguration()),
  Lazy.map((cfg) => cfg.db.connectionString),
); // Lazy<string>

// loadConfiguration() has not executed yet
const url = Lazy.evaluate(databaseUrl); // Evaluated once and cached

When a transformation itself returns a Lazy container, we use chain to flatten the nested context:

const dbConnection = pipe(
  Lazy.from(() => loadConfiguration()),
  Lazy.chain((cfg) => Lazy.from(() => openConnectionPool(cfg.db))),
); // Lazy<ConnectionPool>

const pool = Lazy.evaluate(dbConnection); // Both steps execute once in sequence

Lazy.tap executes a side-effectful callback when the lazy container is evaluated for the first time, passing the computed value through unchanged:

const priceCatalog = pipe(
  Lazy.from(() => computeDetailedPrices(rawCatalog)),
  Lazy.tap((prices) => console.log(`Price list of size ${prices.length} compiled`)),
);

// Nothing has run or logged yet
Lazy.evaluate(priceCatalog); // compute runs, then console.log fires — both once
Lazy.evaluate(priceCatalog); // returns cached value instantly — no console.log

  • Deferring expensive startup computations: Compiling large regular expression suites, parsing extensive localization dictionaries, or validating JSON schemas at application startup degrades boot performance. Lazy postpones execution until a code path explicitly calls for the value, skipping the work entirely if that branch is not reached.
  • Request-scoped derivation memoization: Within an HTTP request handler or calculation pipeline, multiple helper functions may require the same derived data (such as a decoded auth token payload, a compiled discount table, or a permission matrix). Lazy computes the value on the first access and caches the outcome for all subsequent steps within the request lifecycle.
  • Safe deferred dependency pipelines: In modular services, constructing values that depend on other computed settings can trigger eager initialization ordering issues. Lazy allows transformation pipelines to be assembled point-free and evaluated only when downstream consumers require them.