DEV Community

Cover image for Design pattern: JS Functional Chains
Patrick R
Patrick R

Posted on

Design pattern: JS Functional Chains

Functional Chains: Implementation

Writing a serializable chainable functional API in Javascript.

All of the work below can be found in this functional chain builder. A ready-made and reusable npm module allowing you to generate a small API.

Introduction

I've long thought chainable APIs were both elegant and descriptive.

And started playing around with a functional and stateless implementation, as a fun experiment.

The chain

Here's an example the API I'm thinking of :

const operation = multiplyBy(2)
  .and.subtract(6)
  .and.divideBy(2);

operation(33); // => 30
Enter fullscreen mode Exit fullscreen mode

The result should be a re-usable function that applies the different commands in order.

Serialization

Instead of applying the operations immediately, this API is designed to return a function. The reason for that is to allow serialization.

Here's an example of how that would look like :

analyse(operation);

// output =>
[
  { multiplyBy:  [2] },
  { subtract: [6]},
  { divideBy: [2] }
]
Enter fullscreen mode Exit fullscreen mode

What are the benefits of serialization :

Testing

Serialization can be beneficial in testing: we can assert the operations are correct. Possibly replacing end to end tests with simpler unit tests\

Networking

A serialized operation, is one that can be sent over the wire, expanding the use cases of the chain.

Exploiting JavaScript

Let's take a quick look at the language features that allow this to be possible.

Functions are first-class objects

A programming language is said to have First-class functions when functions in that language are treated like any other variable

source: mozilla.org

What does that mean for us:

  • we can pass functions around as arguments
  • we can set properties to functions

Scoping and closures

Closures are simpler to use than they are to explain. But here's what matters to us:

If a function creates another function, that new one can access its creator's scope. It can in turn create a new function itself, and then again, and again... building a chain.

Implementing the chain

Defining the API

Before we actually write the chain, we need to define our api:

const API = {
  add(val) {
    return num => num + val
  },

  subtract(val) {
    return num => num - val
  },

  multiplyBy(val) {
    return num => num * val
  },

  divideBy(val) {
    return num => num / val
  }
}
Enter fullscreen mode Exit fullscreen mode

This is pretty straightforward, each method returns a function that will apply the desired operation.

Creating a wrapper function

We've discussed the idea of returning functions out of functions. So let's create a base function that receives a chain, and returns the completed operation.

function Wrap(chain = []) {
    let compute = (num) => {
        // Iterate through the chain and applies the calculations
        return chain.reduce((mem, fn) => fn(mem), num);
    }

    return compute;
}
Enter fullscreen mode Exit fullscreen mode

At this point, we have no means of adding anything to the chain. So let's add methods to our compute function, one for each that was defined previously.

for (let key in API) {
  const fn = API[key];
  compute[key] = () => {
     ...
  }
}
Enter fullscreen mode Exit fullscreen mode

We already know we need to return a function, that's the expected result of our chain. We also know, that this function should allow more functions to be chained.

Most of you saw this coming, we can simply return our Wrap, which does exactly that. The chaining takes place by providing it an extended chain.

function Wrap(chain = []) {
    let compute = (num) => {
      // Iterate through the chain and applies the calculations
      return chain.reduce((mem, fn) => fn(mem), num);
    }

    for (let key in API) {
      const fn = API[key];
      compute[key] = (num) => {
        return Wrap([ ...chain, fn(num) ]);
      }
    }

    return compute;
}
Enter fullscreen mode Exit fullscreen mode

Currently, this usage would work :

const operation = Wrap()
  .multiplyBy(2)
  .subtract(6)
  .divideBy(2);

operation(33); // => 30
Enter fullscreen mode Exit fullscreen mode

Prettifying our API

We now have a working chainable API. But the need to have Wrap() prefixed to any chain is not of adequate elegance.

Exporting user-friendly methods

We want to be able to start our chain through one of the API's method. An easy way to achieve this is to have our module export those methods, with the wrap included.


// (API Object)

// (Wrap function)

module.exports = Object
    .keys(API)
    .reduce((res, key) => {
      const fn = API[key];
      res[key] = (...params) => Wrap([ fn(...params) ]);
      return res;
    }, {});
Enter fullscreen mode Exit fullscreen mode

We essentially hide the initial wrap inside the methods.

Here's how our usage currently looks :

const { multiplyBy } = require('./mychain');

const operation = multiplyBy(2)
  .subtract(6)
  .divideBy(2);

operation(33); // => 30
Enter fullscreen mode Exit fullscreen mode

Already looking much better.

Adding semantics

Part of our initial design was to have an optional and key word between each chain member. Although the need for that is arguable, let's do it for science.

And the implementation couldn't be any simpler :

function Wrap(chain = []) {
    let compute = (num) => { ... }

    for (let key in API) {
      const fn = API[key];
      compute[key] = (num) => { ... }
    }

    // Semantics of choice
    compute.and = compute;
    compute.andThen = compute;
    compute.andThenDo = compute;

    return compute;
}
Enter fullscreen mode Exit fullscreen mode

Which brings us to our expected usage :

const operation = multiplyBy(2)
  .and.subtract(6)
  .andThen.divideBy(2);

operation(33); // => 30
Enter fullscreen mode Exit fullscreen mode

Next Step: Serialization

Thanks for reading through part one of my functional chain article.

In order to keep them short, I will continue the topic of serialization in a separate article.

If anyone has experience building chainable APIs, I would love to hear your approach and use cases.

Cheers,

Patrick

Top comments (5)

Collapse
 
ironsavior profile image
Erik Elmore

Chain style APIs are a crime against humanity. Get out now and save yourself. This other guy said it better than I can: medium.com/making-internets/why-us...

Collapse
 
nokomoko profile image
nsmokon

but the reason it's not good for lodash is because the chain style API doesn't work well as a means of composing functions

Even though I agree that using functional piping is better than method chaining, can't you just literally add a new method to the API object in this example? Besides the main complain in that medium post seems to be about being unable to do tree shaking with this pattern.

Collapse
 
patrixr profile image
Patrick R

Most of his arguments are against the lodash chain specifically, not the pattern itself.

I don't think he even believes chain style APIs are evil.

I'm not using lodash at all here. It's about the potential vanilla implementation of such pattern.

Which in itself has both good use cases and poor ones. Far from a crime :)

Collapse
 
ironsavior profile image
Erik Elmore

There is a broader wisdom to take from the post I referenced. The lesson is that method chaining as API comes at a cost in terms of design simplicity and consistency. This specific case is explained in terms of lodash chaining, but the reason it's not good for lodash is because the chain style API doesn't work well as a means of composing functions--which is fundamentally what your post is about.

Thread Thread
 
patrixr profile image
Patrick R • Edited

Not denying there are caveats to the pattern. The implementation is still interesting in my eyes.
I appreciate your second comment for the constructive details. My concern is more with click-baity titles :)