October 4, 2023
•
5 min read
The motivation to implement a calculator using function chaining is to demonstrate how this pattern can improve code readability and can logically process actions from left to right. Here is a simple example of how some calculations are implemented with the use of pure functions:
The problem with this calculation is that it is difficult to determine the order of operations at first glance. The deeply nested add
function occurs first, and its result is propagated up to subtract
, then multiply
and finally to divide
.
To tackle this problem we can implement a pattern called function chaining (borrowed from object-oriented programming). Don't worry, if you are not a fan of classes, we can hide the implementation. Ideally, we want to land with something like this:
This syntax is much easier to process logically from left to right. It also provides an easy-to-use auto-completion similar to JavaScript's array methods or string methods.
Note that this implementation does not perform any calculations until calculate()
is run. This is to allow us to create re-usable templates or schemas that can be run only once we run calculate()
. For example:
To only run the calculations on calculate()
we have to ensure that no processing is run when we add()
, subtract()
, multiply()
or divide()
. To do that, we have to keep a queue of operations and process that queue on calculate()
.
Let's start with the baseline implementation using a JavaScript class. Classes are just syntactic sugar over object prototypes but are preferred in TypeScript.
Let's break down everything going on in this code block:
When we instantiate the Calculator
class with the new
keyword, we need to provide a starting number. If no initial value is set, it is defaulted as 0
. The queue is a list of OperationFn
function references that we will execute using the calculate()
method.
add()
, subtract()
, multiply()
and divide()
all accept a number that is used to perform on the running value. The running value is the persistent number (set initially in the constructor). These so-called "operation gatherers" do not perform any mutation on the running value, rather they just add the OperationFn
to the queue for later processing.
add()
, subtract()
, multiply()
and divide()
all return a reference to the class instance by returning this
. This is the magic sauce that allows us to continually call these methods in a chain. Every method call in the chain just adds an operation function reference to this.queue
.
This method finally runs all the operations in the chain iteratively. As you can see, it starts with the initial value and keeps a running result after each operation function call. Note that this.initial
is not mutated and preserved if required in the future.
To use our new calculation we can instantiate our class and chain methods like so:
After every method call, the instance of Calculator
is returned giving us auto-complete like this:
Not a fan of object-oriented JavaScript? The instantiation of the class can be abstracted if you wish like so:
And there we have it. A developer-friendly calculator that processes methods from left to right. Of course, the calculator is a simplified example. The function chaining pattern can be applied to a lot more complex data structures and use cases. My favourite use cases of function chaining in the wild are validation libraries such as zod
, yup
and joi
.