Lua from the ground up

Closures and higher-order functions

Functions as values you can pass and return, and closures that remember the variables they were created beside.

Part 9 of 12

Advanced18 min readBack to Courses

Functions are values

In Lua a function is a value like any other. You can store it in a variable, pass it to another function, and return it from one. A function that takes a function is called a higher-order function.

An unnamed function has no name: a small body wrapped in the function keyword is a value you can hand straight to another function without ever naming it.

Remembering where you were made

A function carries the variables that were in scope where it was written. When it outlives that scope, it keeps its own private copies of them alive, and that pairing of a function with its captured variables is called a closure.

This is how a counter works: one function makes a private number and returns a second function that changes it. Each call to the maker creates a fresh, independent number.

Why it matters

Closures are how you build a small piece of behaviour and keep its state hidden from everything else. Two counters made by the same maker do not share a count, because each captured its own private copy.

They are also the last piece you need before objects. A table full of functions that share hidden captured state is already most of the way to an object, which is the next chapter.

Upvalues are shared, not copied

When a nested function mentions a local from the enclosing function, Lua does not copy the value into the function. It links the function to the variable itself, and that link is called an upvalue. Two closures made in the same call therefore see the same variable and share its changes.

The variable stays alive as long as any closure still points at it. That is why a counter keeps counting after the maker has returned: the frame is gone, but the upvalue it created is still reachable from the function you kept.

Building behaviour from functions

Because a function is a value, you can write one that takes another and decides when to call it. A map function walks a list, applies the function you give it, and returns a new list; a filter returns only the items whose test is true.

The functions you pass can be anonymous. function(x) return x * 2 end is a value with no name, written exactly where it is used. Naming it first is worth it when the same rule is used twice, or when the name explains the rule better than the body does.

Building a function from a value

A maker can take a value and return a function that uses it: function adder(n) return function(x) return x + n end end. adder(10) returns a function that adds ten; adder(1) returns a different function that adds one. Each call captured its own n, so the two do not interfere.

That is partial application: fix part of a job now, supply the rest later. It is useful when a general function needs one configuration value, such as a prefix or a limit, and the caller should not have to repeat it at every call. The returned function is ordinary; nothing marks it as special.

What a closure keeps alive

A captured variable lives on the heap, not in the frame that created it. The first closure that captures a local moves it there, and every closure from the same call shares that one cell. When the last closure becomes unreachable, the collector can release the cell and whatever it points at.

That is why a long-lived closure can hold a large table in memory long after the function that built it returned. Capture the field you need rather than the whole record when the record is big, and drop the closure when the job is done. The cost is invisible in a small program and obvious in a long-running one.

A maker of counters

local function counter()
  local c = 0
  return function()
    c = c + 1
    return c
  end
end

local next_id = counter()
print(next_id())
print(next_id())
print(next_id())

Three closure traps. Expecting two closures from different calls of a maker to share state — only closures made in the same call share an upvalue. Changing a captured variable later and assuming the closure took a snapshot; it sees the current value. And holding a closure that captures a big table long after you are done with it, which keeps that table alive too.

Try these next

  • Write a map that takes a list and a function and returns a new list.
  • Write a filter that keeps only the numbers above a threshold.
  • Make two counters, advance one three times, and print both to prove they are independent.

A counter that remembers

Run it and watch the count climb. Then make a second counter and confirm that the two do not share a number, and pass a small function of your own to double.

Output
Click "Run" to execute your code

An account with two doors

Run it and follow the balance through the two functions. Then change the withdrawal amount so it succeeds, and add a third function that only reports the balance.

Output
Click "Run" to execute your code

A closure keeps its captured variables alive for as long as the function itself is alive. That is exactly what you want for a counter, and worth remembering when a large value seems never to be released.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is a closure?