Functional JavaScript
October 13, 2021
I just finished a Coursera class on SML, so I’ve got functional programming on the brain. And hey! JavaScript can do some functional stuff. Let’s explore that!
Currying
Functions are first-class in functional languages, meaning they can be passed to or returned from other, ‘higher-order’ functions. Passing a function as an argument is pretty common - the array methods ‘map’ and ‘reduce’ are both higher-order functions that apply some provided function parameter to every element of the array. Returning a function isn’t as common in modern programming, but it allows for some neat idioms.
Currying is a technique that’s not going to be super useful on its own in JavaScript, but it’s essential for the next technique. A curried function takes some argument and returns a new function that takes some other argument.
between = x => y => z => z >= x && z <= y;
In this example, between takes a value, x, and returns a function that takes a value, y, which returns a function that takes a value, z, which returns true if z is between x and y. This function would be called with the syntax between(x)(y)(z)
, which probably seems more unwieldy than a function that simply takes three arguments, and is called with between(x, y, z)
. And that’s because it is!
In SML, this idiom allows for the use of fewer parentheses - between would be called as between x y z
, but JavaScript doesn’t allow for parenthesis-less function calls. In JavaScript, currying only really becomes useful in…
Partial Completion
When a curried function is called without providing all of its arguments, it returns a function awaiting the remaining arguments. This is called ‘partial completion.’ For example, if I had a function for which valid inputs must be between 0 and 1, I could write a test function using:
isValid = between(0)(1);
isValid
now holds a function that takes a single argument and returns true if it’s between 0 and 1. It’s called like any other function in JavaScript, isValid(z)
, which can be a bit more succinct and less ambiguous than between(0, 1, z)
or between(z, 0, 1)
.
This idiom really shines when a problem requires several functions that all use similar logic. For example:
isA = between(90)(100);
isB = between(80)(89);
isC = between(70)(79);
isD = between(60)(69);
isF = between(0)(59);
With this little collection, it’d be easy to write a straightforward, legible grading function that returns a letter grade for a given input. This technique is even nicer when working in a REPL, where reusing functions is more common, and remembering them more essential.
Recursion
Recursion is common enough in most languages - functions that conditionally call themselves are really useful for recursive data structures like linked lists or trees. But in functional languages, recursion is used for everything. Languages like SML, Haskell, and OCaml don’t provide for
or while
loop semantics; all looping is done via recursion.
As an example, below is a typical implementation of a factorial function in an imperative style.
function factorial(x) {
let acc = 1;
while (x > 1) {
acc *= x;
x -= 1;
}
return acc;
}
In a functional style, this definition would be:
factorial = (x, acc=1) => x <= 1 ? acc : factorial(x-1, acc*x);
On every loop, the function checks if x is less than or equal to 1; if it is, it returns the accumulator. Otherwise, it calls itself with updated arguments. Once familiar with functional syntax, this style is really easy to read and reason about, especially if formatted a little differently:
factorial = (x, acc=1) =>
x <= 1 ? // Conditional
acc // Left branch, or 'base case'
: factorial(x-1, acc*x); // Right branch
Unfortunately, this isn’t as performant as the while
loop version in modern JavaScript. Almost every feature of ECMAScript 6 has been implemented in every major JavaScript engine; the only exception is ‘tail call optimization,’ which very few engines have implemented, and which is essential for performance in this kind of function.
A tail call is a recursive function call that occurs as the very last step of a function; its return value is not used by the caller. In a typical recurse, function arguments are pushed to a new stack frame, which can be slow, and dangerous - if a function recurses too much, it could overflow the stack and crash the process. With tail call optimization, the caller’s stack frame is reused, its arguments overwritten with the new values.
Until this feature is more common, we’re all stuck using the tall, ugly, brace-filled while
loop version, at least whenever performance is important.
Gimme More
There are two more functional concepts I’d like to discuss: pattern matching and function composition. But this post is long enough already, they’ll have to wait!