April 27th, 2020

⛓️ Method chaining in JS - Functional Programming way

Article cover photo
Photo by Aida L on Unsplash

Remember jQuery? Beside bringing a nice common interface for developers to query the DOM elements, it had some other particular widely used feature. The feature that I would like to focus on is the method chaining:

$(".fancy-welcome-text")
  .first()
  .text("Hello")
  .css("color", "green")
  .fadeIn(1000)
  .text("Hello, World!")
  .fadeOut(1000);

Code that is written in a way where it could be read as a series of steps is nice to follow as it's natural for us to analyze the algorithm step by step, from top to bottom.

This pattern fits nicely when we want to to tame asynchronous nature of JS by making code imitate synchronous program like Promises do:

doSomething()
  .then(result => doSomethingElse(result))
  .then(newResult => doThirdThing(newResult))
  .then(finalResult => {
    console.log(`Got the final result: ${finalResult}`);
  })
  .catch(failureCallback);

However, it's worth noting that asynchronicity is not the only problem that can be solved this way. Take for instance as common built in API as Array and it's methods since ES5:

const blogPosts = [/* some data here */];
const top3blogPostNames = blogPosts
  .filter(post => post.published)
  .slice(3)
  .map(post => post.name);

This reads nice but what if you want to introduce another step (into such chain) which cannot be represented by any Array built-in method? For instance, let's say we would like to join these blog post names (to a string) but we've got some fancier join method than the one which is built in into arrays:

function fancyJoin(elements) {
  if (elements.length < 2) {
    return elements.join(', ');
  }  

  const commaSeparated = elements.slice();
  const lastElement = commaSeparated.pop();

  return `${commaSeparated.join(', ')} and ${lastElement}`;
}

// fancyJoin([1,2]) === '1 and 2'
// fancyJoin([1,2,3]) === '1, 2 and 3'
// fancyJoin([1,2,3,4]) === '1, 2, 3 and 4'

Now, this method cannot be fit nicely into our chain like this:

const top3blogPostNames = blogPosts
  .filter(post => post.published)
  .slice(3)
  .map(post => post.name)
  .fancyJoin() // this does not work that way...

... unless you do some discouraged things like extending Array.prototype.

You can nail such case like this:

const top3blogPostNames = fancyJoin(
  blogPosts
    .filter(post => post.published)
    .slice(3)
    .map(post => post.name)
);

But in that case the flow is no longer readable from top to bottom in terms of order of execution.

What you can do is for instance start using Lodash FP library and compose it's functions using "flow" function:

const { map, filter, take, flow } = require('lodash/fp');

const top3blogPostNames = flow([
  filter(post => post.published),
  take(3),
  map(post => post.name),
  fancyJoin
])(blogPosts);

Nonetheless, after such refactoring some questions may arise 🤔:

1. Is lodash the best option? Maybe X or Y library would be better?

Some older Frontend devs could prefer Underscore, some FP hipsters may say the Rambda is the best choice to go. There is no library that would satisfy everyone, it's a matter of taste 🤷‍♂️

2. Will all JS functions fit nicely in that way?

The function composition works only when subsequent functions take one parameter only. Each function except the first one, consumes the result of the previous one. This means that these functions have to take only one argument as preceding functions can return only single value. Of course you know that functions that take more than one parameter are very common like "take" function which require 2 arguments: how many elements it should take and source array from which elements will be taken.

This problem can be resolved by using curried functions or performing partial application 🧐

3. Is there some way in native JS to do this?

At the moment as I write this blog post there is no officially approved standard. But there are some drafts of ECMAScript feature that is well known in other functional programming languages. That feature is pipeline operator. So how code refactored to the pipeline operator would look like? As we don't have established syntax yet, but there are some proposals and even plugin for Babel let's try to do this and hope that if you are reading this in that future where pipe operator got finalized aren't too much diverged from that what was known for the moment this post was written:

const { map, filter, take } = require('lodash/fp');

const top3blogPostNames = blogPosts
  |> filter(post => post.published)
  |> take(3)
  |> map(post => post.name)
  |> fancyJoin

As you can see, some basic functions like filter and map still have to be reimplemented (or imported from some utility belt libs like lodash like here) to be more FP style, but at the same time we don't need to reimplement the thing that glues different functions (flow for lodash, compose for Redux and Rambda) across different libraries!

What we get here is something that can look like method chaining from the OO world, but which is usable for pure functions. Also, because our "links of a chain" can be no longer tied to the class or specific object prototype, it means that we can compose such chain from any functions that have signature that matches the data of the predecessor.

Conclusion

JavaScript is a language that combines Object-Oriented programming and Functional Programming paradigms, however as we were drifting from OO code to be more FP styled one thing that we can notice is that we tend to simulate some concepts that other functional programming languages have often included out-of-the-box.

Some heavy seasoned programmers may feel confused and even protest the way which JavaScript is heading these days, but it is worth to distinct new features that can be used just to show off ("look how smart I am"), from features that could resolve and unify some generic and domain agnostic problems that are common across JS projects these days.

I am not 100% sure that pipe operator fits nicely to the JS world and if community will adopt it, but I can see some benefits that we can gain by using it:

  • no more this keyword confusion (some people coined the "thisless JS" term for that)
  • low-cost extensibility - you don't have to extend some existing interface, just push your pure function (which can be defined in completely separate module that isn't aware of context in which it's used) through pipe operator