Higher-order Functions

Dear Computer

Chapter 4: Functions

Higher-order Functions

Suppose you are writing a browser-based JavaScript application with three donate buttons, each for a different amount:

You now face the dilemma that every programmer who works with event-driven systems must resolve: you need some of your code to run when the user clicks on a button. But you are not in charge of the button. Someone else wrote its code and the event-handling system years back. How can you get this old code to execute your new code?

First, since you don't want your code to run until a click happens, you delay its execution by dropping it in three different functions:

JavaScript
function donate5() {
  chargeUser(5);
  showThanks();
}

function donate25() {
  chargeUser(25);
  showThanks();
}

function donate100() {
  chargeUser(100);
  showThanks();
}

Second, you register these functions with the event system as callbacks. Only when a click event happens should the functions be called. This is done in the browser by passing the functions to document.addEventListener. However, you must be careful not to call them yourself. This would be bad:

JavaScript
button5.addEventListener('click', donate5());

In many languages, actual parameters are evaluated before their function call. They are eagerly evaluated. You don't want eager evaluation when passing callback functions. To prevent the call in JavaScript, you leave off the parentheses:

JavaScript
button5.addEventListener('click', donate5);
button25.addEventListener('click', donate25);
button100.addEventListener('click', donate100);

A function that receives another function as a parameter is a higher-order function. The browser's addEventListener is a higher-order function. It receives not just data to fill the holes of its algorithm, but also other code.

The repetition across these three donation callbacks is concerning. If you need to add a step to the donation process or fix a bug, you have to make changes in three places. If you want to add a fourth donation amount, you must add an additional callback. You decide to write just one parameterized function that serves all possible amounts:

JavaScript
function donate(amount) {
  chargeUser(amount);
  showThanks();
}

button5.addEventListener('click', donate(5));
button25.addEventListener('click', donate(25));
button100.addEventListener('click', donate(100));

But this change has broken things badly. The user is now charged $130 for just loading your application. That's because you are calling the donate function when you attempt to register the callbacks. You aren't waiting for the click events to happen.

To delay the execution of donate while still allowing its amount to be a parameter, you create a helper function that returns a tailored version of the callback:

JavaScript
function buildCallback(amount) {
  return function donate() {
    chargeUser(amount);
    showThanks();
  }
}

button5.addEventListener('click', buildCallback(5));
button25.addEventListener('click', buildCallback(25));
button100.addEventListener('click', buildCallback(100));

When functions may be passed as parameters, returned, or assigned to variables, they are first-class citizens. They can be treated like any other data type. The buildCallback function is able to return a function because functions are first-class citizens in JavaScript.

Ruby doesn't have first-class functions. Imagine trying to pass puts to a helper function:

Ruby
helper(puts)

This code doesn't pass puts to helper. Recall that Ruby doesn't require parentheses in a function call. The puts function is eagerly called in this code, not passed in an unevaluated form. The nil value it returns is passed to helper.

Ruby does support higher-order functions, but it does so without first-class functions. The most common scheme for passing code around is a block. A block is an anonymous sequence of code that trails the function call. It may receive parameters and return a value, just like a function. Here a block is passed to the sort_by method of the Array class to customize the ordering criteria:

Ruby
words = %w{Mercury Venus Mars Earth Jupiter Saturn Uranus Neptune}
words.sort_by { |word| word.length }

Here an array of 10 elements is initialized to random values using a block with no parameters:

Ruby
randoms = Array.new(10) { rand }

Many Ruby developers adopt the practice of enclosing one-line blocks in curly braces, as you see in the previous two examples. If the block must execute several statements, this do/end syntax is typically used instead. This script rings the terminal bell three times, pausing after each ring:

Ruby
3.times do |i|
  puts "\a"
  sleep 1 
end

When a language supports higher-order functions, many common algorithmic patterns can be factored out and turned into utility functions in the standard library. The parts of these algorithms that vary are left as parametric holes to be filled by code that is passed in from the caller.

← OverloadingFor-each →