Write clean code with the Compose-Combinator
Also, you can't live without the Pipe-Combinator
Combinators are functional patterns that glue (AKA compose) together functions. These patterns promote single responsibility and decoupling.
The Compose Combinator is by far one of the most pervasive combinators around that it's even available in Java - the greatest bastion of Object Oriented programming. It's one of the foundational building blocks of Redux and exists pretty much in every functional library out there.
The Compose Combinator is commonly known as just Compose. It is also known as the Queer Combinator. The Compose Combinator can be defined using lambdas as:
// Lambda definition
(a -> b) -> (b -> c) -> a -> c
// functional definition
const compose = f => g => x => f(g(x));
The Compose Combinator is so important that it is pretty much the fundamental building block of functional programming. In a nutshell, what it does is take two functions and calls them in succession. Given two functions that need to be called in succession, one could write it like this:
function foo(x) {
const result = // do something with x
return bar(result);
}
function bar(x) {
const result = // do something with x
return result;
}
foo("Some Input");
In the above example, you'll notice that foo
and bar
are tightly coupled. This means, if one wanted to test foo
they'll inadvertently need to consider the effects of bar
.
Using the Compose Combinator, the above could be refactored to this:
function foo(x) {
const result = // do something with x
return result;
}
function bar(x) {
const result = // do something with x
return result;
}
compose(bar)(foo)(x)
Now foo
and bar
are decoupled, they can be tested independently. What's even cooler, is that if your language supports a type system, the composition of foo
and bar
can produce different types to ensure that they can only be composed in one direction. Awesome, use that compiler to stop those bugs!
function foo(x: IBarResult): IFooResult {
// do something
return fooResult;
}
function bar(x: int): IBarResult {
// do something
return barResult;
}
compose(bar)(foo)(4); // Compiles!
compose(foo)(bar)(4); // Compilation error!
Pipe Combinator
To those with the keen eye, you'll notice that compose
takes the two functions in reverse order. Mathematicians may say that compose
represents f after g, but for us mere mortals, it might be more convenient to read f before g. When it comes to writing code in English, reading from left to right and top to bottom helps immensely with understanding.
This can be easily achieved using the Pipe Combinator, which, you guess it, is just the Compose Combinator flipped! If you've read my last article on the C-Combinator, you'll know that it's really easy to do this.
To illustrate in Javascript:
const flip = f => y => x => f(x)(y);
const compose = f => g => f(g(x));
const pipe = flip(compose); // same as: g => f => f(g(x));
pipe(foo)(bar)(x);
In some ways, it might make it more readable to use the terms before
and after
, but your mileage may vary. Consider the following:
compose(bar)(foo)(x) === pipe(foo)(bar)(x)
after(bar)(foo)(x) === before(foo)(bar)(x)
You may have already come across the Pipe Combinator if you've used RxJS
or Lodash
(it's known as _.flow
).
Making it Awesomer
One can supercharge their Pipe Combinator by using it as a reducer for a list of functions. In the example below, we can build up a pipeline of functions to calculate the total for a shopping cart:
const pipeline = (...stages) => stages.reduce((f, g) => pipe(f)(g));
const total = pipeline(
totalAllItems,
tee(console.log),
applyHolidayDiscount,
applyProvincialTax,
applyFederalTax,
tee(console.log),
applySeniorsDiscount
)(cart);
That makes your code way easier to understand. One could even inject side effects into this pipeline by using the tee
operator from the C-Combinator blog post. In the above example, logging is done through the tee
operator.
So damn important!
This is such an important combinator that some languages (Haskell
, Elixir
, F#
) have it built in. JavaScript also has it in the form of a Babel plugin, but one day, we'll see it there too.
Hopefully, this has gotten you excited to learn more about combinators! Keep your code decoupled and go out and compose something beautiful! Rock on 🤘!
Bonus! Unit Tests
Below are some unit tests I wrote to experiment with this combinator. I perfer to use tape.js for testing.
const test = require('tape');
const flip = f => y => x => f(x)(y);
const compose = f => g => x => f(g(x));
const pipe = flip(compose);
const fakeFunction = f => x => `${f}(${x})`;
test('pipe', assert => {
assert.plan(2);
const f = fakeFunction('f');
const g = fakeFunction('g');
assert.equal('f(g(x))', compose(f)(g)('x'));
assert.equal('g(f(x))', pipe(f)(g)('x'));
});
test('pipeline', assert => {
assert.plan(1);
const pipeline = (...stages) => stages.reduce((f, g) => pipe(f)(g));
const actual = pipeline(
fakeFunction('f'),
fakeFunction('g'),
fakeFunction('h')
)('x');
assert.equal('h(g(f(x)))', actual);
});