Higher Order Functions
In Scope, Functions and Closures we learned that JavaScript has first class functions and saw examples of functions that both took functions as arguments and retuned functions. As with so many things there's a term for functions that can either take a function as an argument or return a function. Such functions are called Higher Order Functions.
Javascript and popular libraries have many built-in Higher Order Functions that we've all probably used without knowing there was a special name. Some examples follow.
// DOM
button.addEventListener("click", callback);
// Timers
setTimeout(callback, 200);
// Map
[1, 2, 3].map(callback)
// Promises
new Promise(...).then(callback);
// Fetch
fetch(request).then(callback);
// jQuery
$('#my-element').on("click", callback);
// LoDash
_.find([], callback);
Take a Function as an Argument
More interesting than a list of Higher Order Functions is that we can write our own. The question we may ask ourselves is, "Why would we want to write our own Higher Order Functions?" The answer is actually quite simple. Higher Order functions allow us to write common logic a single time and inject custom solutions with each function call.
I think we can agree that a very common need when writing a program is to take an array of data and transform it into a new array of data with the same length. We may have in fact wrote just such a transformation the long way on several occasions.
function myUniqueSolution (arr) {
// Step 1 transform
const copy = [];
for (let i = 0; i < arr.length; i++) {
copy.push(someCustomChange(arr[i]))
}
// more code...
}
The need is in fact so common that many languages have a name for it1. This operation is called map
and you can find implementations of map
in common libraries like LoDash or Ramda and as part of the core language; Array.map
.
Despite the abundance of implementations this need is so common and so well understood that we'll implement it here to demonstrate the value of the "taking a function as an argument" part of Higher Order Functions.
First let's look at the common bits. Every time we call our map
function we'll need to take an input array, instantiate a new array to hold our changed values and loop over the input array in order to apply some transformation.
function map(arr) {
const result = [];
for (let i = 0, ii = arr.length; i < ii; i++) {
result[i] = arr[i];
}
return result;
}
So far map
is nearly the same as our unique solution. In order for it to transform the input data it will need to take a function to change our unique input data into our desired output data.
function map(arr, callback) {
const result = [];
for (let i = 0, ii = arr.length; i < ii; i++) {
result[i] = callback(arr[i], i, arr);
}
return result;
}
// Typescript if you prefer
function map<T, U>(arr: T[], callback: ((arg0: T, arg1: number, arg2: T[]) => U)):U[]
The callback
function is how we encapsulate the unique logic of tranforming some bit of data into a new bit of data. In other words a Javascript function's ability to take a function as an argument allows the developer to focus on truly unique solutions while abstracting away repetitive code.
const fullName = ({ firstName, lastName }) => ({
fullName: `${firstName} ${lastName}`
});
map([
{firstName: "Ian", lastName: "Kilmister" },
{firstName: "Phil", lastName: "Campbell" },
{firstName: "Mikkey", lastName: "Dee" }
], fullName)
While implementations may vary (LoDash) what stays constant is the usefulness of taking a function as an argument in order to customize the behavior of a more generic function. map
can apply any transformation to any set of data without any internal logic related either to the data or the transformation.
Return a Function from a Function
In the early days of a program it may be sufficient to add split functions wherever we need them but it's likely over time that we'll find variant solutions creeping into our codebase. When we have a chance to address our tech debt we'll make an attempt to DRY out these functions.
// a.js
myString.split(' ');
// b.js
myString.split(/\s/);
// c.js
myString.split(/\s+/);
A good first step in refactoring this code might be to start a new .lib
file and export a solution that can accomodate our various needs.
// lib.js
export const REG_SPLIT = /\s+/;
// a.js
myString.split(REG_SPLIT);
// b.js
myString.split(REG_SPLIT);
// c.js
myString.split(REG_SPLIT);
There will come a time however when we want to start splitting strings by different characters like commas or semicolons. At that time we'll also notice that every time we use these regular epressions we're coupling them with the split
method.
// lib.js
export const REG_SPLIT = /\s+/;
export const REG_SPLIT_COMMA = /\s*,\s*/;
export const REG_SPLIT_SEMI = /\s*;\s*/;
// a.js
myString.split(REG_SPLIT_COMMA);
// b.js
myString.split(REG_SPLIT);
// c.js
myString.split(REG_SPLIT_SEMI);
Since we've found ourselves typing .split
quite alot and would like to save ourselves some repetition. The logical next step in refactoring might look like this.
// lib.js
export const splitOnSpace = (string) => string.split(/\s+/);
export const splitOnComma = (string) => string.split(/\s*,\s*/);
export const splitOnSemicolon = (string) => string.split(/\s*;\s*/);
Here again we have a lot of repetition and so we make another attempt at simplification.
// lib.js
const split = (string, regex) => string.split(regex);
export const splitOnSpace = (string) => split(string, /\s+/);
export const splitOnComma = (string) => split(string, /\s*,\s*/);
export const splitOnSemicolon = (string) => split(string, /\s*;\s*/);
And this isn't a whole lot better… so let's re-think it.
Our initial approach to writing the split
function is one I think of as "solution order". I want to split
a String
using a RegeEx
and so I write split = (string, regex)
and this works sensibly in many cases. However each of our functions do the same thing. they take a String
as an argument and call the split
function on the string with the same Regular Expression
.
In many ways this code is not very different from exporting both the split
function and multiple Regular Expression
s and calling them as we need them throughout our program. What we need is a less repetitive solution and for that we need to change the way we think about writing functions.
// lib.js
const split = (regex, string) => string.split(regex);
export const splitOnSpace = (string) => split(/\s+/, string);
export const splitOnComma = (string) => split(/\s*,\s*/, string);
export const splitOnSemicolon = (string) => split(/\s*;\s*/, string);
Reversing the order of the arguments may not seem like much of a change but if we order them from least likely to change between calls to most likely to change between calls we can write our functions a bit differently.
// lib.js
const split = ( regex ) => ( string ) => string.split(regex);
// more explicit example
function split (regex) {
return function (string) {
return string.split(regex)
}
}
Let's pause here a moment to consider our newest implemention of split
.
split
is a function that takes a Regular Expression
as an argument and returns a function. The function returned by split
takes a String
and returns the result of splitting the String
with the Regular Expression
. The implicit return of arrow functions make this syntax conventient to write but sometimes less obvious than a more explicit example.
split
is a Higher Order Function because it returns a function. The following example shows how we would use it.
// lib.js
export const splitOnSpace = (string) => split(/\s+/)(string);
export const splitOnComma = (string) => split(/\s*,\s*/)(string);
export const splitOnSemicolon = (string) => split(/\s*;\s*/)(string);
Refactoring our various split functions we find we're still doing a lot of typing. Even worse we're seeing the weird ()()
syntax again. It sure would be nice to get rid of that.
// lib.js
export const splitOnSpace = split(/\s+/);
export const splitOnComma = split(/\s*,\s*/);
export const splitOnSemicolon = split(/\s*;\s*/);
If you recall from Scope, Functions and Closures Javascript functions can be assigned to variables—or of course you may know that from elsehwere. splitOnSpace
and the others are functions. In fact each is the function retuned by calling split
with a Regular Expression
. And yet each function is unique because because the closure created by calling split
saves a reference to the Regular Expression
split
was called with. In turn each invocation of splitOnSpace
accesses /\s+/
and applies it to a String
argument.
splitOnSpace("the quick brown"); // ['the', 'quick', 'brown']
splitOnSpace("fox jumped over"); // ['fox', 'jumped', 'over']
splitOnSpace("the lazy dog."); // ['the', 'lazy', 'dog.']
Ultimately the advantage of writing Higher Order functions that return functions is that we can write generic functions like split
and from them generate specific functions like splitOnSpace
. Additionally saving the various Regular Expressions
in a closure instead of a global variable gives us certainty that they won't be changed elswhere in our programs since a function's scope can't be accessed from outside of the function.
The downside of our last implemention of split
is the ()()
syntax. We'll explore the built-in way to address this next and other solutions in the future.
Function.bind
Most of the time we'll be happy to split strings on known characters but there's bound to be a time in the future where we'll have to split a string on some unique sequence. This single solution won't be universal enough to merit addition to our lib
file so we may want to do it in place. At that time we're going to have to decide wich is better, funny syntax or more constants.
// unique.js
export const uniqueSplit = split(uniqueReg);
// confusing syntax
split(uniqueReg)(string);
Reusing split
would a better developer experience if we could sometimes call it like split(reg, string)
and other times call it like split(reg)(string)
. The native way to do that in JavaScript is using Function.bind
.
.bind
is a method on the Function
object that takes one or more arguments. The first argument is always the this
context you want the bound function to be called with. We won't work with this
so in our example we always call .bind(null, ...)
.
The rest of the arguments are a comma separated list of arguments in the order in which you'd like them to apply. The return value of .bind
is a new function that will execute the bound function with the initially supplied arguments and arguments supplied to the bound function in left to right order.
That's a lot to stuff into your brain all at once so let's look at an example. First we write our split function as a single function like we normally would keeping in mind that the left to right order of our arguments will be important later.
const split = ( regex, string ) => string.split(regex);
Writing normal syntax means we'll have no shennanigans when we want to use .split
in some unique situation elsewhere.
// unique.js
import { split } from "./lib.js";
// elsewhere
split(uniqueReg, string);
Leveraging .bind
allows to generate the same functions as earlier.
// lib.js
export const splitOnSpace = split.bind(null, /\s+/);
export const splitOnComma = split.bind(null, /\s*,\s*/);
export const splitOnSemicolon = split.bind(null, /\s*;\s*/);
Under the hood .bind
is doing basically what we did earlier manually. .bind
has advantages like allowing us to write our functions normally and handling functions with any amount of arguments. But it also has the disadvatage of compelling us to consider a this
context that our functions will never use.2
In the future we'll explore how to write our own utility functions that can replace the need to use either .bind
or this
.