Skip to content

Instantly share code, notes, and snippets.

@dillonforrest
Last active May 31, 2020 16:18
Show Gist options
  • Save dillonforrest/6097657 to your computer and use it in GitHub Desktop.
Save dillonforrest/6097657 to your computer and use it in GitHub Desktop.
Discussion of various types of chaining in JavaScript.

Chaining

Chaining in javascript can come in many forms. Here, I'll discuss method chaining, lazy chaining, and pipelining. My aim is to help me, and maybe you, understand these chaining options. This gist is inspired by Michael Fogus's O'Reilly book 'Functional JavaScript.' Like Fogus's book, I'll assume use of underscore.js as a supplement. If you're interested in functional programming in javascript, I highly recommend Fogus's book.

A quick example

What do I mean by 'chaining'? Let's look over a quick example just to be on the same page. This example uses underscore.js's _.chain method.

function findFruitCosts(groceries) {

  return _.chain(groceries)
    .filter(function(item) {
      return item.type == 'fruit';
    })
    .map(function (fruit) {
      return fruit.price;
    })
    .reduce(function (subtotal, price) {
      return subtotal + price;
    }, 0)
    .value();

}

With the underscore.js library, chaining is a very explicit function. But chaining is really any series of consecutively called functions, in which the results of the previous function affect the inputs of the next function. Fun, yes?

Now, let's dive right into the different types of chaining.

Method chaining

The most common form of chaining. You've likely seen jQuery chains.

$('#button').on('click', function () {
  $(this)
    .toggleClass('clicked')
    .slideToggle()
    .find('.links')
      .toggle();
});

Method chaining comes with an object-oriented paradigm. Usually, the methods share the same reference. In javascript, that means method chains tend to rely on the this keyword.

var Calculator = function () {
  this.amount = 0;
}

Calculator.prototype.add = function (n) {
  this.amount += n;
  return this;
};

Calculator.prototype.subtract = function (n) {
  this.amount -= n;
  return this;
};

Calculator.prototype.getAmount = function () {
  return this.amount;
};

var calculator = new Calculator();

var amt = calculator.add(5).add(10).subtract(1).subtract(2).getAmount();
console.log(amt);
//=> 12

This is the same type of chaining used with jQuery. Note that to enable a function to be method-chainable, we end each function with return this;.

Method chaining is so easy to use because of the prevalence of an object-orientated codebase centered around dynamic references to this. The reason for its ease of use also is its downfall. Each method on the method chain reads from and mutates a common reference. Steering away from object methods, I'd like to talk next about...

Lazy chaining

A lazy chain won't run even though I've composed the chain. It only runs when I tell it to, and that might be far after I've composed it. Let's revisit my previous underscore example. The _.value method not only returns the value, but also terminates the chain. However, if I don't call _.value, all the previous operations have already run. The underscore chain is not lazy. A lazy chain would wait until a function similar to _.value was called.

Let's look at Fogus's example of a lazy chain.

function LazyChain(target) {
  this._calls = [];
  this._target = target;
}

You construct the lazy chain by first creating a new instance, then adding new functions to the chain using the invoke method:

LazyChain.prototype.invoke = function (methodName /*, args */) {
  var args = _.rest(arguments);
  
  this._calls.push(function (target) {
    var method = target[methodName];
    return method.apply(target, args);
  });
  
  return this;
};

The invoke method also has return this; at the bottom. So we can chain it. However, this is not a class instance like it is with jQuery or the previous calculator example. Instead, this is the lazy chain itself. The _calls register holds onto methods to be applied to a passed target. Each method which waits to be called is known as a 'thunk.'

After we've constructed our lazy chain with one or more calls to invoke, we can execute the chain by running force:

LazyChain.prototype.force = function () {
  return _.reduce(this._calls, function (target, thunk)) {
    return thunk(target);
  }, this._target);
};

Here's Fogus's example of the full lazy chain in action:

new LazyChain([2,1,3])
  .invoke('concat', [8,5,7,6])
  .invoke('sort')
  .invoke('join', ' ')
  .force();

//=> "1 2 3 5 6 7 8"

Cool! You can build a chain indefinitely and execute it whenever you want. However, all methods still mutate a shared reference. If you wanna avoid side effects, pipelining will be a good alternative.

Pipelining

Pipelining can be lazy or not, depending on whether or not you wrap another function around everything. In a proper pipeline function, we should be able to take in a data object and all functions with which to chain.

function pipeline(seed /*, functions */) {
  var functions = _.rest(arguments);
  return _.reduce(functions, function (target, fn) {
    return fn(target);
  }, seed);
}

Pipelining is cool because most of the custom functionality comes from the inputs. We can just pass different functions. That means we can leverage partials and currying to create dynamic and expressive pipelines and a functional api.

Closing thoughts

There are other types of chaining which I didn't include, such as promises. In the end, all chains can be used as tools to make your code smaller, more testable, and more extensible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment