First-Class Functions in JavaScript

Ian Grubb
10 min readSep 28, 2020

--

Photo by Nick Fewings on Unsplash

JavaScript is a multi-paradigm programming language that allows certain functional programming techniques. It does this by supporting the use of first-class functions — functions that can be stored in variables and treated like data. JavaScript functions can even be passed to other functions as inputs, and they can be returned from other functions as outputs. This is a seemingly small feature, but it can have enormous consequences for how you write code.

Unfortunately, many JavaScript newcomers have a hard time understanding the functional side of the language. Functional programming requires concepts and techniques that you won’t necessarily have encountered before, even if you have a significant background in object-oriented programming.

Nonetheless, learning functional programming can make you a better JavaScript developer. Whether you’re working on a vanilla JavaScript app, an app in a front-end framework like React, or a back-end built with Node, functional techniques can be used to build new features and write cleaner code. So if you want to learn more about functional JavaScript, join me in this quick crash course.

Putting Functions into Functions

Because JavaScript supports first-class functions, it lets us write functions that accept other functions as arguments. These functions that get passed into other functions are typically referred to as callbacks. Passing a callback into another function allows us to tell that function what code to run at certain points in its execution. Let’s look at a couple of the cases where callbacks are useful.

Controlling Asynchronous Behavior

Perhaps the most common use for callbacks is controlling code that should get executed at some point in the future. Consider the following function, which starts a timer and logs a message once time is up:

const sendDelayedMessage = (message, seconds) => {
const sendMessage = () => console.log(message)
setTimeout(sendMessage, seconds * 1000)
}

In this example, the setTimeout function expects a callback, which acts like a plan for what to do after the specified number of seconds has elapsed. That plan is represented by sendMessage, which is passed as an argument to setTimeout.

Note that sendMessage couldn’t have just been console.log(message).That would have logged the message immediately and left sendMessage undefined. It may look a bit weird have the arrow and empty parentheses as part of sendMessage, but they are essential in order to delay the execution of console.log. Functions like this are sometimes referred to as thunks, and it’s not uncommon to have to thunk parts of your code so that you can have control over when it gets executed.

Working with Array Methods

Callbacks are also useful when working with iteration methods like map and filter. All of these methods accept a callback function and then call that callback for each element in the array that they’re called on. What sets them apart from each other is what they do with the return values that they get from the callback.

The map method makes a 1-to-1 mapping between the elements of the original array and elements of a new array. For instance, the code [1, 2, 3].map(x => x + 1) returns the array [2, 3, 4] — every number in the new array is 1 plus the corresponding number in the previous array. Under the hood, map makes a new array, calls the function x => x + 1 on every number in the old array, and pushes each of the return values into the new array.

The filter method works a bit differently. Its purpose is to select a subset of the elements in the initial array, based on whether they satisfy some condition. To do this, it calls the callback function on each element in the array and check whether it returns a truthy value. In other words, the callback function acts as a condition that array elements can either satisfy or not. As an example, a call like [1, 2, 3].filter(x => x % 2 === 1) returns [1, 3], which are the odd elements found in the initial array.

Array methods like map and filter illustrate how a functional approach can lead to cleaner code. Iterating with for loops tends to be pretty verbose and requires code that gets repeated for many other for loops. Array methods hide that repeated code, resulting in programs that are more succinct and easier to read.

Designing Functions that Receive Callbacks

In the examples so far, we’ve designed callbacks that would achieve an intended effect when passed to pre-built JavaScript functions. However, you should also look out for cases where you can prevent code duplication by writing functions designed to accept callbacks.

Let’s take inspiration from the array methods we just looked at and write one of our own. I’m going to write a function that takes the elements in an array and groups them according to an attribute type of my choice. For instance, I should be able to take the array ["I <3 functional programming”, “Arrows are great", "Learn functional JavaScript"] and group these sentences by their word counts, returning the object {3: [“Arrows are great", "Learn functional JavaScript"], 4: ["I <3 functional programming”]}.

I can do this with a function that accepts an array and a callback as arguments. It loops over the array and calls the callback for every element, building up a grouping object one element at a time:

const groupBy = (array, callback) => {
const grouping = {}
for (let i = 0; i < array.length; i++){
const value = array[i]
const key = callback(value)
if (grouping[key]){
grouping[key].push(value)
} else {
grouping[key] = [value]
}
}
return grouping
}

We could now use this function as follows:

const getWordCount = string => string.split(" ").length
const sentences = ["I <3 functional programming”, “Arrows are great", "Learn functional JavaScript"]
groupBy(sentences, getWordCount)

and it would return the sentences grouped by word count.

Getting Functions Out of Functions

We can also write functions that return functions as their outputs. These returned functions can then be saved in variables and used elsewhere in a program. This means that rather than writing all of a program’s functions prior to runtime, we can write certain functions that dynamically generate other functions as our code executes.

Currying

Here’s an interesting consequence of allowing functions to return functions: it makes it possible to rewrite any program so that each of the functions in the program has at most one argument. This is possible because of a technique known as currying.

It’s easiest to see how this works with an example. If we started with the function const addNormal = (x, y) => x + y, we could curry it by keeping the return value the same but splitting the arguments up with multiple arrows: const addCurried = x => y => x + y. This curried function takes a number and returns the function y => x + y, which then takes a second number and returns a sum. Both functions allow us to add two numbers, they’re just called in slightly different ways:

addNormal(5, 7)
// 12
addCurried(5)(7)
// 12

As you can see, the normal function accepts its arguments as a pair, while the curried function accepts its arguments one after another.

Curried functions give us added flexibility, since they let us apply some but not all of the arguments to a function. We have the option of passing addCurried 5 and 7 together, but we could also just pass 5 and hold off on passing the second argument. Instead, we would store the intermediate function in a variable and then call this function later on:

const addFive = addCurried(5)
addFive(7)
// 12

Now we have a special type of adding function that we can use multiple times across a program, potentially helping us clean up our code.

Closures

Let’s look more closely at how currying is possible. The addFive function behaves like the function y => 5 + y. However, that’s not what addFive actually looks like — if you were to log it, you’d see the result y => x + y. So how can this function act like y => 5 + y?

The answer has to do with the concept of closure. When a function gets defined, it has access to the variables in the surrounding environment. If it references a variable that’s defined outside of its own scope, it remembers the variable and can reference the value in the future when the function is called. This means it’s best not to think of addFive as just the function y => x + y, but rather this function and a reference to the variable x. Since the variable continues to hold the value 5, it gets substituted into the function whenever it gets called.

It’s worth emphasizing that a closure remembers the variable it references, not the value that was stored in the variable when the function was defined. It’s hard to appreciate the difference in the previous case, since x is a constant that will always contain the value 5. However, it’s also possible to create a closure that points to some mutable state, resulting in a function that can change its behavior over time.

To illustrate this concept, let’s make a function that returns a counter function. The counter function it returns should simply return the number of times it has been called whenever it gets called. Here’s the function:

const makeCounter = () => {
let count = 0
return () => count++
}

The returned function () => count++ references the variable count, which is defined outside of its scope. It also mutates the value of that variable, meaning that () => count++ won’t always have the same return value. Here’s how you might use makeCounter:

const counterOne = makeCounter()counterOne()
// 1
counterOne()
// 2
const counterTwo = makeCounter()counterTwo()
// 1
counterOne()
// 3

So makeCounter functions as an initializer that you call to return a counter that you can save in a variable. The counters it makes keep track of their value over time, but different counters enclose different counter variables, meaning that they increase independently of each other.

Functions that Enhance Other Functions

To conclude, let’s look at some functions that have other functions as both inputs and outputs. You can use these kinds of higher-order functions to take in ordinary functions and return versions of those functions that have been enhanced in various ways.

I’ll start with a function that lets us time how long it takes for some other function to finish executing. This could be useful for testing code performance. What we need is a function that takes a callback and returns a modified function. The modified function should start a timer, call the callback function with its arguments, report how much time has elapsed, and then return the callback’s return value:

const addTimer = callback => {
return (...args) => {
const initialTime = Date.now()
const returnValue = callback(...args)
console.log(`Duration: ${Date.now()- initialTime}ms`)
return returnValue
}
}

A handy trick here is using ...args, which will match any number of arguments passed into a function and put them into an array. This lets us build a general-purpose addTimer function that can add a timer to a function with any number of arguments.

It might also be useful to have functions that track how they get used in an application. We can do this by writing a function that updates another function to keep track of its own call history:

const trackHistory = callback => {
const history = []
return (...args) => {
const returnValue = callback(...args)
history.push({ arguments: args, result: returnValue})
console.log(history)
return returnValue
}
}

This higher-order function declares a history variable outside the scope of the returned function, so that this function can remember its old calls and push data about these calls whenever they happen. Here’s an illustration of what this can do in practice:

const add = (x, y) => x + y
const trackedAdd = trackHistory(add)
trackedAdd(1, 2)
// logs: [{arguments: [1, 2], result: 3}]
trackedAdd(5, 7)
// logs: [{arguments: [1, 2], result: 3}, {arguments: [5, 7], result: 12}]

This could be useful for debugging, or even the basis for a rudimentary caching strategy.

We can actually improve on this technique. It’s annoying that the current function logs its history in every case, and it would be great to have separate ways to view and control the history. We can do that with this higher-order function:

const controlledTracking = callback => {
let history = []
const updatedFunction = (...args) => {
const returnValue = callback(...args)
history.push({ arguments: args, result: returnValue})
return returnValue
}
const report = () => console.log(history)
const clear = () => history = []
return [updatedFunction, report, clear]
}

The basic strategy is the same as before, only now we define three functions and return them all in an array. You can use these as follows:

const add = (x, y) => x + y
const [controlledAdd, report, clear] = controlledTracking(add)
controlledAdd(1, 2)
controlledAdd(5, 7)
report()
// logs: [{arguments: [1, 2], result: 3}, {arguments: [5, 7], result: 12}]
clear()
report()
// logs: []

For convenience, we’re defining the trio of functions by pattern matching on the array returned by controlledTracking. This gives us an automatically caching version of the original function, a function to view the current state of the cache, and a function to clear the cache.

Finally, let’s return to the topic of currying. We can always manually curry functions ourselves, but it’s more convenient to have a higher-order function that automatically curries other functions for us:

const curry = callback => {
const recursiveReturn = currentArgs => nextArg => {
const newArgs = [...currentArgs, nextArg]
if (newArgs.length >= callback.length) {
return callback(...newArgs)
} else {
return recursiveReturn(newArgs)
}
}
return recursiveReturn([])
}

The output function lets us feed in arguments over time and save them to an array whenever we do. Whenever the array doesn’t contain enough arguments to call the callback function, the function just returns a copy of itself, except that the copy knows about the new argument. Once there are enough elements in the array, the callback function gets called with elements of the array as its arguments.

--

--

Ian Grubb

Full stack web developer and educator. Former software engineering coach at Flatiron School and adjunct professor in philosophy at NYU.