Higher-order Functions
Suppose we are writing a browser-based JavaScript application with three donate buttons, each for a different amount:
We now face the dilemma that every programmer who works with event-driven systems must resolve: we need some of our code to run when the user clicks on a button. But we are not in charge of the button. Someone else wrote its code and the event-handling system years back. How can we get this old code to call code we are just writing now?
First, since we don't want our code to run until a click happens, we delay its execution by dropping it in three different functions:
function donate5() {
chargeUser(5);
showThanks();
}
function donate25() {
chargeUser(25);
showThanks();
}
function donate100() {
chargeUser(100);
showThanks();
}
function donate5() { chargeUser(5); showThanks(); } function donate25() { chargeUser(25); showThanks(); } function donate100() { chargeUser(100); showThanks(); }
Second, we 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, we must be careful not to call them ourselves. This would be bad:
button5.addEventListener('click', donate5());
button5.addEventListener('click', donate5());
In many languages, actual parameters are evaluated before they are passed. They are eagerly evaluated. That means donate5
is getting called before the donate button even gets clicked. We don't want eager evaluation when passing callback functions. To prevent the call in JavaScript, we leave off the parentheses:
button5.addEventListener('click', donate5);
button25.addEventListener('click', donate25);
button100.addEventListener('click', donate100);
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 we need to add a step to the donation process or fix a bug, we have to make changes in three places. If we want to add a fourth donation amount, we must add an additional callback. We decide to write just one parameterized function that serves all possible amounts:
function donate(amount) {
chargeUser(amount);
showThanks();
}
button5.addEventListener('click', donate(5));
button25.addEventListener('click', donate(25));
button100.addEventListener('click', donate(100));
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 our application. That's because we are calling the donate
function when we attempt to register the callbacks. We aren't waiting for the click events to happen.
To delay the execution of donate
while still allowing its amount to be a parameter, we create a helper function that returns a tailored version of the callback:
function buildCallback(amount) {
return function donate() {
chargeUser(amount);
showThanks();
}
}
button5.addEventListener('click', buildCallback(5));
button25.addEventListener('click', buildCallback(25));
button100.addEventListener('click', buildCallback(100));
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:
helper(puts)
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:
words = %w{Mercury Venus Mars Earth Jupiter Saturn Uranus Neptune}
words.sort_by { |word| word.length }
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:
randoms = Array.new(10) { rand }
randoms = Array.new(10) { rand }
Many Ruby developers adopt the practice of enclosing one-line blocks in curly braces, as we 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:
3.times do |i|
puts "\a"
sleep 1
end
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.