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
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] }
]
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
}
}
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;
}
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] = () => {
...
}
}
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;
}
Currently, this usage would work :
const operation = Wrap()
.multiplyBy(2)
.subtract(6)
.divideBy(2);
operation(33); // => 30
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;
}, {});
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
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;
}
Which brings us to our expected usage :
const operation = multiplyBy(2)
.and.subtract(6)
.andThen.divideBy(2);
operation(33); // => 30
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)
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...
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.
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 :)
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.
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 :)