Closures and upvalues

Lua · Practical · 10 min

Closures power callbacks, iterators, memoisation and simple objects throughout Lua code.

Lua has lexical scoping: a function can read and write the local variables of the blocks that enclose it, even after those blocks have finished. A captured variable is called an upvalue.

Each time a function expression is evaluated, a new closure is created with its own references to the captured variables. Two closures created in the same call share the same upvalue, so they see each other's updates.

This gives you private state cheaply. A counter factory returns a function that increments a local; nobody outside can touch the count. Memoisation is a closure over a cache table. Callbacks passed to an event system capture the context they need.

Loops create a fresh local for each iteration: closures created inside for i = 1, 3 each capture their own i, unlike var in old JavaScript.

Scope rules worth remembering:

  • A local is visible from the statement after its declaration to the end of its block.
  • local function f makes f visible inside its own body, so it can recurse; local f = function does not.
  • do ... end creates a block purely to limit scope.

Accessing upvalues is slightly slower than locals but much faster than globals, which is why performance-sensitive code often caches globals in locals (local insert = table.insert).

Example

local function counter()
  local n = 0
  return function() n = n + 1; return n end,
         function() return n end
end
local inc, get = counter()
inc(); inc()
print(get())                 --> 2 (shared upvalue)

local fns = {}
for i = 1, 3 do fns[i] = function() return i end end
print(fns[1](), fns[3]())    --> 1	3 (fresh i per iteration)

local function memoize(f)
  local cache = {}
  return function(x)
    local v = cache[x]
    if v == nil then v = f(x); cache[x] = v end
    return v
  end
end
local slow_sq = memoize(function(x) print("computing", x); return x * x end)
print(slow_sq(4))            --> computing	4 / 16
print(slow_sq(4))            --> 16 (cached)

Remember

  • A closure captures enclosing locals (upvalues) by reference
  • Closures created in one call share the same upvalues
  • Each loop iteration gets a fresh loop variable

Watch out for

  • Using local f = function() ... f() ... end for recursion: inside the body f is still the global (nil).

Check yourself

  1. Two closures returned from the same call to counter() both use n. What happens when one increments it?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.lua.org/manual/5.4/manual.html#3.5

Closures and upvalues · Lua · Topolearn