Skip to content

Lazy Evaluation

FxTS provides lazy evaluation. Let me explain through code why lazy evaluation is useful.

We often see code like the one below. By writing code declaratively, we want to make code that is maintainable and easy to read.

ts
const sum = (a: number, b: number) => a + b;

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  .filter((a) => a % 2 === 0)
  .map((a) => a * a)
  .reduce(sum);

It looks very readable. Now let's see how it works.

To treat it as immutable, each time the method proceeds, an array of a new size is created and the array is traversed.

typescript
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  .filter((a) => a % 2 === 0) // [0, 2, 4, 6, 8]
  .map((a) => a * a) // [0, 4, 16, 36, 64]
  .reduce(sum); // 120

Because it iterates through all the array values, the logic that reduces the size of the array, such as slice and filter, is usually placed in front of the logic (That way you can have fewer traversals).

typescript
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  .filter((a) => a % 2 === 0) // [0, 2, 4, 6, 8]
  .slice(0, 2); // [0, 2]
  .map((a) => a * a) // [0, 4]
  .reduce(sum); // 4

Currently, the size of array is very small, so it doesn't seem like a problem. But if the size gets really big, do we have to go back to imperative programming?

FxTS can be used as a combination of functions that deal with Iterable/AsyncIterble, in which case it evaluates the value from the Iterable/AsyncIterable only as needed.

take(2)(only 2 values) are evaluated and no further values are evaluated after that. In addition, the above code Array.prototype.filter needs to traverse all values, while the code below only evaluates the values it needs. Even the filter.

typescript
pipe(
  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
  filter((a) => a % 2 === 0), // [0, 2]
  map((a) => a * a), // [0, 4]
  take(2), // [0, 4]
  reduce(sum), // 4
);

FxTS are a useful way to represent large or possibly infinite enumerable data sets

typescript
pipe(
  range(Infinity),
  filter((a) => a % 2 === 0), // [0, 2]
  map((a) => a * a), // [0, 4]
  take(2), // [0, 4]
  reduce(sum), // 4
);

Combinations of Lazy functions don't evaluate actual values like generator. It can be evaluated with a for-of or await for-of, Strict functions. Strict functions can be found here

typescript
const squareNums = pipe(
  range(Infinity),
  map((a) => a * a),
); // not evaluated not yet

const result = pipe(
  squareNums,
  filter((a) => a % 2 === 0),
  take(10),
  toArray, // Strict function
);

Lazy functions can be found here

Useful Example

The code below shows a more useful situation.

typescript
/**
 * [{
 *   title: string,
 *   director: string,
 *   language: string,
 *   genre: string,
 *   rating: number,
 *   ...
 * }]
 */
const fetchMovie = async (year: number) =>
  fetch(`https://api.movie.xxx/${year}`);

const recommendMovie = async (year: number, rating: number) =>
  pipe(
    range(year, Infinity),
    toAsync,
    map(fetchMovie),
    map((res) => res.json()),
    filter((movie) => movie.rating >= rating),
    head,
  );

await recommendMovie(2020, 9);

What about native Iterator Helpers?

ES2025 shipped lazy Iterator Helpers (Iterator.prototype.map, filter, take, ...) — for synchronous sequences, the platform now does what this page describes. FxTS is the layer above that floor:

  • Async laziness — async iterator helpers are still a proposal, while FxTS runs the same operators over AsyncIterable today, including early termination across asynchronous work.
  • Concurrency — native helpers evaluate one value at a time; concurrent and concurrentPool control how many asynchronous values are evaluated at once.
  • Composition — curried, data-last functions compose in pipe with Promise-unwrapping type inference, instead of method chains on iterator instances.

See Handle Concurrency and Process LLM Token Streams for where this matters most.

Released under the Apache-2.0 License.