The introduction

I’ve written about lazy evaluation implementations in JS in two previous articles, but both have been dabble in, with no intention of scaling horizontally. The relevant work is already done (such as lazy.js), so it doesn’t make much sense for me to do it again. I saw a similar library on GitHub this week, using a native Generator/Iterator implementation, which is still very popular. I see someone is still writing, I will try. I then wrote the library in a Douglas Crockford style of programming to see if it would work.

Crockford advocates no this and prototype chains, no ES6 Generator/Iterator, no arrow functions… Data encapsulation is implemented using factory functions.

Douglas Functions

First, if we do not use the ES6 Generator, we have to implement a Generator ourselves, which is relatively simple:

function getGeneratorFromList(list) {
  let index = 0;
  return function next() {
    if (index < list.length) {
      const result = list[index];
      index += 1;
      returnresult; }}; }/ / example:
const next = getGeneratorFromList([1.2.3]);
next(); / / 1
next(); / / 2
next(); / / 3
next(); // undefined
Copy the code

ES6 gives arrays the [symbol.iterator] property, which gives behavior to the data, making it easy to perform lazy evaluation. Without the convenience of ES6, we have to manually convert data to rows. Here’s how to do it:

function Sequence(list) {
  const iterable = Array.isArray(list)
    ? { next: getGeneratorFromList(list) }
    : list;
}
Copy the code

If you pass a native array to a Sequence, it passes the array to getGeneratorFromList, generating a Generator that completes the data-to-behavior conversion

After the two core functions are written, let’s implement a map:

function createMapIterable(mapping, { next }) {
  function map() {
    const value = next();
    if(value ! = =undefined) {
      returnmapping(value); }}return { next: map };
}

function Sequence(list) {
  const iterable = Array.isArray(list)
    ? { next: getGeneratorFromList(list) }
    : list;

  function map(mapping) {
    return Sequence(createMapIterable(mapping, iterable));
  }

  return {
    map,
  };
}
Copy the code

After the map is written, we also need a function to convert the behavior back to data:

function toList(next) {
  const arr = [];
  let value = next();
  while(value ! = =undefined) {
    arr.push(value);
    value = next();
  }
  return arr;
}
Copy the code

Then we have a half-complete library of lazy evaluation, although it now only maps:

function Sequence(list) {
  const iterable = Array.isArray(list)
    ? { next: getGeneratorFromList(list) }
    : list;

  function map(mapping) {
    return Sequence(createMapIterable(mapping, iterable));
  }

  return {
    map,
    toList: (a)= > toList(iterable.next);
  };
}

/ / example:
const double = x= > x * 2 // The arrow function can be used in this way
Sequence([1.3.6])
  .map(double)
  .toList() / /,6,12 [2]
Copy the code

Adding a filter method to the Sequence is nearly complete, and extending the other methods is easy.

function createFilterIterable(predicate, { next }) {
  function filter() {
    const value = next();
    if(value ! = =undefined) {
      if (predicate(value)) {
        return value;
      }
      returnfilter(); }}return {next: filter};
}

function Sequence(list) {
  const iterable = Array.isArray(list)
    ? { next: getGeneratorFromList(list) }
    : list;

  function map(mapping) {
    return Sequence(createMapIterable(mapping, iterable));
  }

  function filter(predicate) {
    return Sequence(createFilterIterable(predicate, iterable));
  }

  return {
    map,
    filter,
    toList: (a)= > toList(iterable.next);
  };
}

/ / example:
Sequence([1.2.3])
  .map(triple)
  .filter(isEven)
  .toList() / / [6]
Copy the code

It looks like it’s okay to continue with the above example.

The problem

I went on to write a dozen functions, such as take, takeWhile, concat, zip, etc. Until I didn’t know what to write next, then I referred to the lazy.js API and gasped. Lazy.js is about 200 apis, and you have to document after you write the code. I really don’t want to do this anymore. The bigger problem was not the workload, but the sheer number of apis that made me realize the problem with my approach.

When you implement chained calls using factory functions, each call returns a new object that contains all the apis. Suppose there are 200 apis, and every time you call one of them, you get 199 of them thrown away… You can’t do that with enough memory. I’m obsessive-compulsive and can’t stand the waste.

The conclusion is that if you want to implement chain calls, it’s better to use a prototype chain.

But is the chain call itself okay? Although the chained invocation with the prototype chain can save the object creation of subsequent calls, it is also inevitable to waste memory during initialization. For example, there are 200 methods on the prototype chain, and I only call 10 of them. The remaining 190 are not needed, but they are still created at initialization.

I’m thinking about the API changes in rx.js from version 5 to version 6.

// rx.js 5
Source.startWith(0)
  .filter(predicate)
  .takeWhile(predicate2)
  .subscribe((a)= > {});

// rx.js 6
import { startWith, filter, takeWhile } from 'rxjs/operators';

Source.pipe(
  startWith(0),
  filter(predicate),
  takeWhile(predicate2)
).subscribe((a)= > {});
Copy the code

RxJS 6 uses pipe composition instead of chain calls. After this change, you can refer to any operator you want, without having to initialize any redundant operators, which is also good for tree shaking. So let’s emulate the Rxjs 6 API and rewrite the Sequence library above.

Implement lazy evaluation with a combination of pipes

The implementation of the operators is not much different from the above. The main difference is that the way the operators are combined has changed:

function getGeneratorFromList(list) {
  let index = 0;
  return function generate() {
    if (index < list.length) {
      const result = list[index];
      index += 1;
      returnresult; }}; }function toList(sequence) {
  const arr = [];
  let value = sequence();
  while(value ! = =undefined) {
    arr.push(value);
    value = sequence();
  }
  return arr;
}

// The Sequence function itself is very lightweight, and operators are introduced on demand
function Sequence(list) {
  const initSequence = getGeneratorFromList(list);
  
  function pipe(. args) {
    return args.reduce((prev, current) = > current(prev), initSequence);
  }
  return { pipe };
}

function filter(predicate) {
  return function(sequence) {
    return function filteredSequence() {
      const value = sequence();
      if(value ! = =undefined) {
        if (predicate(value)) {
          return value;
        }
        returnfilteredSequence(); }}; }; }function map(mapping) {
  return function(sequence) {
    return function mappedSequence() {
      const value = sequence();
      if(value ! = =undefined) {
        returnmapping(value); }}; }; }function take(n) {
  return function(sequence) {
    let count = 0;
    return function() {
      if (count < n) {
        count += 1;
        returnsequence(); }}; }; }function skipWhile(predicate) {
  return function(sequence) {
    let startTaking = false;
    return function skippedSequence() {
      const value = sequence();
      if(value ! = =undefined) {
        if (startTaking) {
          return value;
        } else if(! predicate(value)) { startTaking =true;
          return value;
        }
        returnskippedSequence(); }}; }; }function takeUntil(predicate) {
  return function(sequence) {
    return function() {
      const value = sequence();
      if(value ! = =undefined) {
        if (predicate(value)) {
          returnvalue; }}}; }; } Sequence([2.4.6.7.9.11.13]).pipe(
  filter(x= > x % 2= = =1),
  skipWhile(y= > y < 10),
  toList
); / / [11, 13]
Copy the code

Reference:

Let’s experiment with functional generators and the pipeline operator in JavaScript