Haskell-like composition in JavaScript

Dimitri Nikogosov
DailyJS
Published in
4 min readJul 10, 2020
Photo by Nathan Dumlao on Unsplash resembles a composition of functions.

Functional composition applies one function to the results of another: const f = x => g(h(x)). In the example functions g and h are composed. In Haskell notation it can be rewritten as follows: f = g . h, where . is an infix compose function.

It is ok to have just one nested pair of parentheses in your code, but when you need to compose multiple functions it is better to use a higher-order function like Ramda’s compose. I prefer to write it line by line with trailing comma:

import R from 'ramda';const f = R.compose(
g,
h,
m,
n,
p,
)

This code is equivalent to the following less readable and modifiable code:

const f = x => g(h(m(n(p(x)))))

What if we can compose functions in JS like in Haskell:

const f = g . h . m . n . p;

It looks concise, easy to read, fast to write. The best part is that it’s possible in JS.

Compose like a boss

In JS, the Proxy object is a metaprogramming tool that gives us an ability to override fundamental JS operations. It allows us to interfere in the various internal JS processes like property access, function application etc. In JS, the dot is for property value access. To modify this behaviour we shall use a get trap:

const composable = {
get: function(target, prop) {
if (prop in target) {
return target[prop];
} else {
const entity = eval(prop);
if (typeof entity === 'function'
&& typeof target === 'function') {
return (...args) => target(entity(...args));
}
}
}
};

Now we can apply a proxy to a particular function to extend its’ behaviour because functions are objects in JS.

const double = new Proxy(function (x) {
return x * 2;
}, composable);
const addThree = new Proxy(x => x + 3, composable);const f = double . addThree . double;f(1.5) === 12 // true

It works as expected, good. However, I want to write a normal JS function rather than proxy functions.

We would better modify Function.prototype to make all functions composable in our code this way, but metaprogramming in JS is not as powerful as we need, at least now. Function.prototype cannot be modified.

Here comes recursive Proxy

If I want all functions to be extended, I need to return proxy composition from the get trap:

const composable = {
get: function(target, prop) {
if (prop in target) {
return target[prop];
} else {
const entity = eval(prop);
if (typeof entity === 'function'
&& typeof target === 'function') {
return new Proxy(
(...args) => target(entity(...args)),
composable
);
}
}
}
};

Here the handler object references itself when returns a proxied composition. A kind of recursive Proxy.

With that implementation, we need only the leftmost function in a composition chain to be proxied. It will ‘infect’ all following functions with the same getter trap. Also, to avoid writing a proxy wrapper around the first function in my code every time I want to make a composition, I need some predefined function which I can place in any composition chain without changing the semantics of it. The best candidate is an identity function:

const id = new Proxy(x => x, composable);

The identity function can be composed multiple times without affecting the final result of a chain. Now we can compose ordinary functions in JS through the dot:

// It works with function declaration syntax
function double (x) { return x * 2; }
// It works with arrow functions
const addThree = x => x + 3;
const f = id . double . addThree . double;fn(1.5) === 12 // true

You can even write it line-by-line for easy modification, debugging and git blame purposes:

const f = id
. double
. addThree
. double
;

This feature even lets you compose named functions with anonymous ones:

const f = id
[ x => x * 2 ]
[ x => x + 3 ]
. double
;

Note that there is no dot before the opening square bracket.

Drawbacks

This feature is a neat demonstration of JS metaprogramming feature and even can be used in real code, but it has several downsides:

  • The leftmost function in a composition chain must be proxied with the getter trap.
  • Anonymous function composition has weird syntax with this implementation — square brackets instead of a dot.
  • The solution doesn’t support anonymous functions with free variables that are not in the same scope as the getter trap.

--

--

Dimitri Nikogosov
DailyJS
Writer for

MD | Clinical geneticist | Bioinformatician | Functional programming enthusiast