Implementing lazy evaluation in JavaScript with streams

Lazy evaluation is a programming technique that defers the evaluation of an expression until its value is actually needed. This can be particularly useful when working with large datasets or performing complex calculations, as it allows for more efficient memory usage and improves performance.

In JavaScript, we can implement lazy evaluation using streams. Streams are a sequence of values that can be processed one at a time. The values in the stream are computed on-demand, as they are called for by the consumer.

Creating a Stream

To implement lazy evaluation with streams in JavaScript, we can create a custom Stream class. The Stream class should provide methods to generate values and perform operations on the stream.

Let’s start by creating a simple Stream class:

class Stream {
  constructor(generatorFn) {
    this.generatorFn = generatorFn;
  }

  [Symbol.iterator]() {
    return this.generatorFn();
  }
}

The Stream class takes a generatorFn as its constructor parameter. This function should generate the values for the stream. We also implement the [Symbol.iterator] method to make the stream iterable.

Lazy Evaluation with Streams

Now that we have our Stream class, we can use it for lazy evaluation. We can define various operations on the stream, such as map, filter, and reduce.

Map Operation

The map operation applies a function to each value in the stream and returns a new stream with the transformed values. Here’s an example implementation:

Stream.prototype.map = function (fn) {
  const self = this;

  return new Stream(function* () {
    for (const value of self) {
      yield fn(value);
    }
  });
};

Filter Operation

The filter operation applies a predicate function to each value in the stream and returns a new stream with the values that pass the filter. Here’s an example implementation:

Stream.prototype.filter = function (predicate) {
  const self = this;

  return new Stream(function* () {
    for (const value of self) {
      if (predicate(value)) {
        yield value;
      }
    }
  });
};

Reduce Operation

The reduce operation reduces the stream into a single result by applying a reducer function to each value in the stream. Here’s an example implementation:

Stream.prototype.reduce = function (reducer, initialValue) {
  const self = this;

  let accumulator = initialValue;

  for (const value of self) {
    accumulator = reducer(accumulator, value);
  }

  return accumulator;
};

Example Usage

Now let’s see an example of using our Stream class for lazy evaluation:

function* generateNumbers() {
  let i = 0;
  while (true) {
    yield i++;
  }
}

const numbers = new Stream(generateNumbers());

const evenSquares = numbers
  .filter((n) => n % 2 === 0)
  .map((n) => n * n);

const result = evenSquares.reduce((sum, n) => sum + n, 0);

console.log(result);  // Output: 166650

In this example, we generate an infinite stream of numbers using a generator function. We then apply filter to get only the even numbers and map to square each number. Finally, we use reduce to sum up the squared even numbers.

By using lazy evaluation with streams, we are able to work with infinite collections without the need to generate and store all the values upfront. This improves memory efficiency and allows for more flexible and performant code.

#lazyevaluation #JavaScript