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:
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:
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:
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:
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:
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)
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 }
Here an array of 10 elements is initialized to random values using a block with no parameters:
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:
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.