AV

Andrew Vo-Nguyen

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:

```
1function add(valueA: number, valueB: number) {
2 return valueA + valueB;
3}
4
5function subtract(valueA: number, valueB: number) {
6 return valueA - valueB;
7}
8
9function multiply(valueA: number, valueB: number) {
10 return valueA * valueB;
11}
12
13function divide(valueA: number, valueB: number) {
14 return valueA / valueB;
15}
16
17const calculation = divide(multiply(subtract(add(3, 2), 1), 5), 2);
18
19console.log(calculation); // 10
```

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:

`1calculator(3).add(2).subtract(1).multiply(5).divide(2).calculate();`

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:

```
1const taxSchema = (amount: number, taxRate: number) => calculator(amount).multiply(taxRate);
2
3// At at later date we can run the calculation
4const tax = taxSchema(100, 0.15).calculate();
5console.log(tax); // 15
```

In order to only run the calculations on `calculate()`

we have to ensure that no processing is run when we `add()`

, `subtract()`

, `multiply()`

or `divide()`

. In order 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.

```
1function add(valueA: number, valueB: number) {
2 return valueA + valueB;
3}
4
5function subtract(valueA: number, valueB: number) {
6 return valueA - valueB;
7}
8
9function multiply(valueA: number, valueB: number) {
10 return valueA * valueB;
11}
12
13function divide(valueA: number, valueB: number) {
14 return valueA / valueB;
15}
16
17type OperationFn = (value: number) => number;
18
19class Calculator {
20 private initial: number;
21 private queue: OperationFn[];
22
23 constructor(initial = 0) {
24 this.initial = initial;
25 this.queue = [];
26 }
27
28 add(valueB: number) {
29 this.queue.push((valueA) => add(valueA, valueB));
30 return this;
31 }
32
33 subtract(valueB: number) {
34 this.queue.push((valueA) => subtract(valueA, valueB));
35 return this;
36 }
37
38 multiply(valueB: number) {
39 this.queue.push((valueA) => multiply(valueA, valueB));
40 return this;
41 }
42
43 divide(valueB: number) {
44 this.queue.push((valueA) => divide(valueA, valueB));
45 return this;
46 }
47
48 calculate() {
49 let result = this.initial;
50 for (const operation of this.queue) {
51 result = operation(result);
52 }
53 return result;
54 }
55}
```

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:

```
1const result = new Calculator(3).add(2).subtract(1).multiply(5).divide(2).calculate();
2
3console.log(result); // 10
```

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:

```
1const calculator = (initial = 0) => new Calculator(initial);
2
3const result = calculator(3).add(2).subtract(1).multiply(5).divide(2).calculate();
4
5console.log(result); // 10
```

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`

.