Introduction
A mind is a terrible thing to waste, and one of the fastest ways to do it is not keeping it open enough. Although most of my development career I’ve spent writing in JavaScript, and I love this language, I’ve never considered myself a JavaScript developer. I never had problems with writing in Java, Python, or C# when there was a need, and I also like to explore languages I don’t know about and try to get the best out.
This was precisely the case with Elixir — one of my colleagues in my previous company talked me into taking an online course about Elixir, and I liked it very much — it has cool language features like list comprehensions and advanced pattern matching. Still, the one that I’ve resonated most with was definitely the pipe operator. Unfortunately, I didn’t have much opportunity to write in Elixir at work, but I found ways to use what I’ve learned while writing in JavaScript. Read on to find out!
Currying
The first concept I’d like to familiarize you with to get going is function currying (the name, surprisingly, not coming from a popular Indian dish but rather from an American mathematician and logician, Haskell Curry). I’ll explain what it’s useful for in a moment. For now, all that we need to know is that currying a function with a fixed number of arguments makes it possible to pass arguments in more than one call. Seems complicated? Perhaps the example will make it clear:
import { curry } from 'ramda';
const addThreeNumbers = (a, b, c) => a + b + c;
addThreeNumbersCurried(1, 2, 3); // 6
addThreeNumbersCurried(1)(2, 3); // 6
addThreeNumbersCurried(1, 2)(3); // 6
addThreeNumbersCurried(1)(2)(3); // 6
Of course, we’d need to either implement the curry
function ourselves, or — like in the example above — use an existing implementation, in this case, the one that’s part of a functional library called ramda
, which I will also use in some of the following examples.
Composition
The second useful concept is function composition. What’s that about? According to the mathematical definition, if there’s a function f(x)
and another g(x)
, it’s possible to compose them into a function h(x) = g(f(x))
. Let’s imagine that we need to add two to a number, then raise the result to the power of two, and from the resulting number, subtract three.
const performCalculations = (myNumber) => {
return Math.pow(myNumber + 2, 2) - 3;
}
The code isn’t that complex, but it’s really not that apparent what is going on in there at first sight; the readability is not great. Also, what happens if we need to add a similar calculation but with slightly different parameters (like adding five, raising to the power of three, etc.)? Apart from not being readable, the code is also not very reusable. What can we do to improve it? Let’s start with extracting all the atomic actions.
const add = (a, b) => a + b;
const power = (base, exponent) => Math.pow(base,exponent);
const subtract = (a, b) => a - b;
This would allow us to write the code in a bit more reusable way:
const performCalculations = (myNumber) => {
return subtract(power(add(myNumber, 2), 2), 3);
}
This definitely is more reusable, but still not very readable — and we’re only doing three modifications to the input. What would happen if we had more? Let’s try to make that more understandable:
import { compose } from 'ramda';
const performCalculations = compose(
(n) => subtract(n, 3),
(n) => power(n, 2),
(n) => add(n, 2) );
What happened here? We took three functions, like f(x), g(x), and h(x), and we’ve created a brand new function that’s a composite of the three: k(x) = h(g(f(x))). The order of execution is exactly like in the mathematical definition — from the inside out. We start with the last function that we want to compose and proceed toward the first. While some people like that approach, I consider it counterintuitive — in my opinion, it needs an additional mind cycle to adjust. Luckily, there’s a simple way to make it a bit more straightforward.
Plumbing
import { pipe } from 'ramda';
const performCalculations = pipe(
(n) => add(n, 2),
(n) => power(n, 2), (n) => subtract(n, 3)
);
What we see now is a bit more natural — the order of execution is the same as the natural reading order — we know exactly what’s happening at first glance. Why pipe? It’s simply because we pipe data through consecutive functions to get the final result. Can it be simplified even further? Let’s go back to our atomic functions for a moment.
import { curry } from 'ramda';
const add = curry((b, a) => a + b);
const power = curry((exponent, base) => Math.pow(base, exponent));
const subtract = curry((b, a) => a - b);
Two apparent things happened here. First, we’ve reversed the order of arguments in each function (it actually isn’t needed for add
— because of the commutativity of addition — but this way, all the functions are consistent), and second, we’ve currified all the functions. What for? Let’s take a look at how this can affect our calculations:
import { pipe } from 'ramda';
const performCalculations = pipe(
add(2),
power(2),
subtract(3)
);
The first thing that’s clearly visible is that this function is now readable as plain English — we add two, then raise to the power of two, and subtract three! This is primarily possible thanks to currying. Let’s take a look at the power
function:
import { curry } from 'ramda';
const power = curry((exponent, base) => Math.pow(base, exponent));
power(2, 3); //9
power(2)(3); //9
As you can see, currified power
can now accept two arguments at once or — if only supplied with the first one, which is now the exponent, returns a function that takes a base and raises it to the previously specified power.
What is that good for? Well, in my opinion — first and foremost, it increases readability, composability, and maintenance. The readability improvement is unquestionable — if the piped functions are named correctly (or semantically), the code should read like plain English. Composability gains are also pretty obvious — if there’s ever a need to do raise a number to the power of five and then add seven, it wouldn’t be required to write the whole code from scratch. The same goes with maintainability — if there’s a need to add three instead of two, it’s instantly clear how to change the values in the pipe, whereas it’s pretty easy to make a mistake while editing Math.pow(myNumber + 2, 2) — 3
.
I hope that you’ll find any of this useful — if so (or not!), please let me know!