Back

Activity 1.8: Closures and Scope

divider

The Idea

Two facts you already have. A function is a value (Activity 1.7). And a local is only visible inside the block that declared it (Activity 1.2). Put them together and something genuinely useful happens.

A function defined inside another one can still see the outer locals — even after the outer function has finished and returned.

main.lua
local function makeGreeter(greeting)
return function(name)
return greeting .. ", " .. name
end
end
local hello = makeGreeter("Hello")
local hey = makeGreeter("Hey")
print(hello("Ada"))
print(hey("Bo"))
Output
Hello, Ada
Hey, Bo

By the time hello("Ada") runs, makeGreeter finished long ago. Its greeting should have been gone. It is not, because the returned function still holds onto it. A function bundled together with the variables it captured is called a closure, and it is the most powerful thing in this unit.

Note also that each call made its own separate copy. hello and hey do not share a greeting.

Captured variables can be changed

This is where it gets interesting. The closure does not get a snapshot — it gets the variable itself, and it can write to it.

main.lua
local function makeCounter()
local count = 0
return function()
count = count + 1
return count
end
end
local tick = makeCounter()
print(tick())
print(tick())
print(tick())
Output
1
2
3

count survives between calls and remembers where it got to. It is also genuinely private — there is no name you could write outside makeCounter that reaches it. No keyword made it private. It is private because of where it was declared.

Two functions, one captured variable

Closures made in the same place share what they captured. This is how you build something with several operations and hidden state.

main.lua
local function makeBay()
local docked = 0
local function arrive()
docked = docked + 1
return docked
end
local function depart()
docked = docked - 1
return docked
end
return arrive, depart
end
local arrive, depart = makeBay()
print(arrive())
print(arrive())
print(depart())
Output
1
2
1

arrive and depart are looking at the same docked. If you have used objects in another language, notice what you just built without any object syntax: a thing with two methods and one private field. Activity 1.11 does this a different way, and it is worth remembering that this way existed first.

Closures drive for-in loops

Activity 1.6 covered three loops. There is a fourth form, for x in f do, and it works by calling a function over and over until it returns nil. A closure is exactly what you need to write one.

main.lua
local function countdown(n)
return function()
if n > 0 then
n = n - 1
return n + 1
end
end
end
for t in countdown(3) do
print("T-minus", t)
end
Output
T-minus 3
T-minus 2
T-minus 1

The loop calls the returned function, gets 3, then 2, then 1, then nil — and stopping on nil is the entire contract. This matters more than it looks: ipairs and pairs, which you will use constantly from Activity 1.9 on, are not special syntax. They are ordinary functions doing what you just did.

Why this is the page that pays off in Unit 2

A game is full of things that need to remember something between frames without announcing it to the rest of the program: a spawn timer, a reload delay, a cooldown, a wave counter. Every one of those is a closure, and the alternative is a global, which Activity 1.2 already talked you out of.

divider

Build

Start a fresh main.lua. You are building a docking-bay ticket machine, then your own loop.


Task 1: A counter that remembers

  • Write makeCounter(label) which returns a function.
  • The returned function should add one to a private count and give back the label with the number attached.
  • Make one counter and call it twice.
main.lua
-- main.lua - Skynest bay counter
local function makeCounter(label)
local count = 0
return function()
count = count + 1
return label .. " #" .. count
end
end
local bayA = makeCounter("Bay A")
print(bayA())
print(bayA())
Output
Bay A #1
Bay A #2

Task 2: Prove they are independent

  • Make a second counter with a different label.
  • Interleave the calls — A, A, B, A.
  • Predict Bay B's number before running it. If you expect it to be 3, you are thinking of a shared variable, and the output will correct you.
main.lua
-- main.lua - Skynest bay counter
local function makeCounter(label)
local count = 0
return function()
count = count + 1
return label .. " #" .. count
end
end
local bayA = makeCounter("Bay A")
local bayB = makeCounter("Bay B")
print(bayA())
print(bayA())
print(bayB())
print(bayA())
Output
Bay A #1
Bay A #2
Bay B #1
Bay A #3

Task 3: Write your own loop

  • Write countdown(n) that returns a function counting down from n.
  • When it runs out, let it return nothing — falling off the end of a function returns nil, which is the signal the loop is waiting for.
  • Drive it with for t in countdown(3) do.
main.lua
-- main.lua - Skynest bay counter
local function makeCounter(label)
local count = 0
return function()
count = count + 1
return label .. " #" .. count
end
end
local bayA = makeCounter("Bay A")
local bayB = makeCounter("Bay B")
print(bayA())
print(bayA())
print(bayB())
print(bayA())
local function countdown(n)
return function()
if n > 0 then
n = n - 1
return n + 1
end
end
end
for t in countdown(3) do
print("T-minus", t)
end

Challenge (Optional)

  • Build makeBay() returning two functions that share one private count, as in the third figure. Then add a third that only reports the count without changing it.
  • Write makeCooldown(frames) that returns a function which reports false the first few times it is called and true after that. You will genuinely use this in Unit 2 — it is how a weapon stops firing every frame.
  • Make countdown count up to a limit instead, then check whether you have accidentally written the numeric for loop from scratch.
divider

Check Your Work

Your finished file should produce this.

Output
Bay A #1
Bay A #2
Bay B #1
Bay A #3
T-minus 3
T-minus 2
T-minus 1

Bay B is on #1 while Bay A is on #3. That is two independent private variables, created by the same three lines of code, with no class and no object anywhere. You have now met the hard half of Lua. The rest of the unit is one data structure.