Skip to content

Step 1 of 5 · Reading · ~3 min

Learn

Functions

Closures

A Lua function does not only remember its parameters. It remembers the local variables that were in scope where it was written, and it keeps them alive for as long as the function itself lives. Those captured variables are called upvalues, and a function together with its upvalues is a closure.

Every function in Lua is a closure. There is no second kind.

The classic shape

local function makeCounter()
    local n = 0
    return function()
        n = n + 1
        return n
    end
end

local tick = makeCounter()
print(tick())
print(tick())
print(tick())

--> 1
--> 2
--> 3

makeCounter returned long ago and its stack frame is gone — but n is not. The inner function captured it, so n lives on as private state that nothing outside the closure can name, read, or corrupt.

Each call captures its own

Call the maker twice and you get two independent counters, because each call creates a fresh n.

local a = makeCounter()
local b = makeCounter()
print(a())
print(a())
print(b())

--> 1
--> 2
--> 1
Rendering diagram…

Two closures can share one upvalue

When two inner functions are created in the same scope, they capture the same variable — not a copy each.

local function makeAccount(balance)
    local function deposit(amount)
        balance = balance + amount
    end
    local function get()
        return balance
    end
    return deposit, get
end

local put, show = makeAccount(100)
put(50)
put(25)
print(show())

--> 175

balance is a parameter, and parameters are locals, so it gets captured like any other. This is how you build an object with genuinely private fields in a language that has no private keyword: the data is not hidden by a rule, it is unreachable because no name for it exists outside.

Are loop variables captured per iteration?

Languages disagree on this one, and the answer decides whether a loop full of closures is useful or useless.

local fns = {}
for i = 1, 3 do
    fns[i] = function() return i end
end
print(fns[1]())
print(fns[3]())

--> 1
--> 3

Each pass of a for loop gets a fresh copy of the control variable, so every closure captured a different i. The same holds for any local declared inside the loop body.

✗ a local declared outside the loop is a single variable shared by every closure made inside it — occasionally what you want, usually the bug.

Your exercise

Make a Counter asks for counter(start): a function returning a function, where each call to the inner one yields the next integer, beginning at start.

Read "beginning at start" carefully, because that is exactly where the example above differs from what the exercise needs. makeCounter increments before returning, so with n starting at 0 its first result is 1. Copy that shape as-is and your first result becomes start + 1: the visible test passes start of 1 with 3 calls and expects 1 2 3, but you would print 2 3 4. Either grab the current value before incrementing, or seed the upvalue one below start.

The starter also supplies the driver loop and the print, so do not print from inside the closure. If you print in there and return nothing, the driver's own print receives no values and adds a blank line after each number: the output comes out 1, blank, 2, blank, 3, blank. Every number is correct and the run still fails — which is the hardest kind of failure to spot.

Up nextErrors: error, assert, pcallFunctions

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…