Why do we need mathematical function composition in Javascript(or any computer language)?

KF
5 min readDec 24, 2019

--

Functions have been an integral part of basic algebra for at least 2000 years. Every branch of mathematics from Algebra to Trigonometry to Calculus has the functions as an essence. Modern computer science, in its most of the parts, is heavily based on the mathematics that we, humans, have invented through the development of thousands of years. What makes the Computer Science interesting is that it makes the mathematical theorems executable and applicable to a variety of utilities.

Composition in photography

Functions are declarative by nature. They are always pure in mathematics. They can be composed( result of one function can be chained to the input of another function). In maths, we define a function as:

f(x) = 2x + 3
g(x) = x - 1

These two functions can be combined as:
f(g(x)) = f.g(x)

Even any number of functions can be chained this way. Every function is independent, pure and the result only depends on the input provided. In maths, dot(.) is the operator to compose functions. We can also follow the same old and tested mathematical notion in our computer code.
Suppose we have two javascript functions:( I will use typescript to explicitly specify the types of variables and functions )

const add2 = (x: number) => x +2;
const multiplyBy3 = (x: number) => x * 3;

The above two functions are pure and can be composed the same way as in mathematics.
multiplyBy3(add2(5));

The most important factor to keep in mind while composing any functions is that the types must match in the chain of the functions. The type of output of inner function must match the type of input of the outer function. In our case type of functions align: ( I have used Hindley-Milner type notation )

add2:: number -> number
multiplyBy3:: number -> number
multiplyBy3(add2(5)):: number -> number

Types aligned, wooh!

Did you notice there is a problem with this approach of function composition? If we keep composing many functions, our code will become messy and very hard to understand.
sine(findLogBase10(findSquareRoot(multiplyBy3(add2(5)))));

Better Way to compose functions

Just like the dot operator of mathematics, we can also derive our own compose operator to appear the composition linear and understandable rather then messy.
Below is the code to compose two functions:

const compose = (f, g, x)=> f(g(x));

Now we can use our own compose function to compose the above defined functions as:

const addThenMultiply = compose(multiplyBy3, add2, 5);

Doesn’t it look odd to put the input to the compose function? Let’s rewrite the compose in a better/curried way:

const compose = (f, g) => x => f(g(x));

Now this compose function can be used as:

const addThenMultiply = compose(multiplyBy3, add2)(5);

At first, this will seem to be an extra work just to compose two functions if we can already do it using function application notation. A good argument, indeed. But you failed to look further down the road, my friend!

Composition with any number of functions

Now, here comes the real power of our compose function. It can make clumsy looking tens of level nested function composition to a linear row. Let’s first redefine our compose function:

const compose = (…fns) => v => fns.reduceRight((arg, fn) => fn(arg), v);

At first, it looks little daunting, but it is very straightforward, if you have used reduce ever before. Reduce function folds an array to some value iterating all the values from left to right. ReduceRight folds an array to some value iterating all the values from right to left. Now we can compose as many functions as we want in a linear manner:

const composed = compose(sine, findLogBase10, multiplyBy3, add2);

In mathematics, function composition is associative:
f.(g.h) = (f.g).h
Same holds true in our compose function as well.
compose(sine, compose(multiplyBy3, add2)) = compose(compose(sine, multiplyBy3), add2);
Isn’t it great that the years old axioms still hold true?

Type Alignment

Till now we have composed the functions which have same input and output type. But we can have functions of different input and output types. There is only condition for composition is that the types of consecutive functions must align. Let’s check the types of the below functions:

f: A -> B ( f is a function that accepts input of type A and returns output of type B)
g: B -> C
( g is a function that accepts input of type Band returns output of type C)

These two functions can be composed in two ways. But only one will be valid.
compose(f, g) is invalid because the return type of ‘g’ doesn’t align with the input type of ‘f’.

compose(g, f) == g(f) :: A -> C
Type of composed function would be A -> C

This process can be extended to any number of functions till the types are aligned.
f: A -> B
g: B -> B
h: B -> C
compose(h, g, f):: A -> C

In compose, the functions are executed in reverse order. This is in sync with how these are executed in mathematics. In maths, f.g executes function ‘g’ first and then function ‘f’. For our convenience we can reverse the order of the composition. This operator is called ‘pipe’ in programming languages.

pipe(f, g, h) == compose(h, g, f) these two operators serve the same purpose.

Defining types of compose function

I will use typescript to define the types of compose and pipe function. Typescript is really a great tool to provide type safety and compiler checking to our javascript code.
For compose function we have to provide overloaded type definitions as the typescript doesn’t support a proper way like haskel to specify.

We can declare as many overloaded declarations as we want the cardinality of our compose function.

--

--

KF

Interested in beginnings rather than end, gives values to past not to repent but to analyze, learning history of philosophy and philosophy of mathematics.