In 1.07 Making functions we learned about, well, making functions. So what about higher-order functions? What are they and how do we make them? Simply put, higher-order functions are functions built on top of other functions. Here's a basic example:
local run_twice = function(some_function, some_data) some_function(some_data) some_function(some_data) end run_twice(print, 'Hello World!')
It can take any function and run it twice for you, in this case the
local twice = function(fn, val) return fn(fn(val)) end local add_four = function(num) return num + 4 end return twice(add_four, 12)
Take a look at the bottom line for a second.
We are calling the function
twice with two arguments, the
add_four function and the number
The purpose of the
twice function is to take a value,
12 in this case, and run it through the given function (
Now take a look inside the
Inside it returns
Given what we know is being passed to this function, this can be read as saying
The order of operation says to start from the inner-most parenthesis and work your way out:
and that is what is returned when you run the code.
The power of these higher-order functions is that they are re-usable.
You can give the
twice function anything that takes and returns a value:
local twice = function(fn, val) return fn(fn(val)) end local double = function(number) return number * 2 end return twice(double, 3)
...or similar to our original example:
local twice = function(fn, val) return fn(fn(val)) end local shout = function(message) print(message .. '!!') return message end return twice(shout, 'hello')
There are all examples of higher-order functions that accept a function as an argument. Another kind of higher-order function is one that returns another function:
local wrapper = function() return function() return 'You found the treasure!' end end local kinder_surprise = wrapper() local secret = kinder_surprise() return secret
When we ran
wrapper it returned us another function that we had to invoke to get to the innermost value.
To avoid all the variable names, you can save some time and invoke such kinds of functions like so:
local wrapper = function() return function() return 'You found the treasure!' end end return wrapper()()
Which number will print out by running the following code?
local number = 3 local closure = function() local number = 5 return function() print(number) end end local print_number = closure() print_number()
Ok, so let's try a this same function-returning-a-function thing but passing in some data:
local adder = function(a) return function(b) return a + b end end local add_three = adder(3) return add_three(1)
add_three variable is assigned a unique and special function.
It is assigned the inner function within the adder function, but with the data we passed in now assigned to the
Even though the function was returned outside of the scope it was defined in, the scope's data was enclosed inside the returned function until the function was discarded and the program exited.
These types of functions are common in situations where a function needs to be generated multiple times but with different data sets.
The data in the closure can also continue to be updated, giving you the ability to make storage containers for your data. Try this out:
local make_counter = function() local number = 0 return function() number = number + 1 return number end end local count = make_counter() print(count()) print(count()) print(count()) print(count())
In programs like LÖVE there are callback systems where a similar effect happens:
local entity = require('entity') love.draw = function() entity:draw() end
As seen in the previous chapter, the
love.draw callback is defined in a main.lua file and later invoked somewhere within the game engine.
love.draw was defined in the scope where the entity variable is defined, the entity variable lives on and can be used inside
love.draw long after the main.lua file is done being invoked.
Closures take some practice to understand and appreciate, but once you see practical examples of where and how to use them they become an indispensable item on your programming toolbelt. In the previous section we used the term composite data to compare primitive and non-primitive data types. In this section we saw how to go about composing higher-order functions. In the following pages we will cover some higher-order functions that are the building blocks for old and modern software alike.
make_counterexample above, try generating multiple counters:
-- Do the numbers in each counter stay in -- sync or are they tracked independently? local count_a = make_counter() local count_b = make_counter()
Using the same
make_counterexample, modify it to return a table instead of a function. Within this table, define an
decrementfunction so that you can make the counter number go up or down. How would you use such a function?