Closures and Higher-order Functions
In Lua, functions are first-class values and can be assigned to variables or treated as arguments and return values of other functions. A closure is a function that retains variables from an outer scope (upvalues), enabling flexible design when combined with higher-order functions.
Syntax
-- -----------------------------------------------
-- Basic closure syntax
-- -----------------------------------------------
-- Captures a variable from the outer scope (upvalue)
local function outer()
local upvalue = 0 -- upvalue captured by the closure
return function() -- returns an anonymous function as a closure
upvalue = upvalue + 1
return upvalue
end
end
local counter = outer() -- obtain the closure as a counter
print(counter()) -- 1
print(counter()) -- 2
-- -----------------------------------------------
-- Basic higher-order function syntax (takes a function as argument)
-- -----------------------------------------------
-- Receives fn and applies it to each element of the list, returning a new table
local function map(tbl, fn)
local result = {}
for i, v in ipairs(tbl) do
result[i] = fn(v) -- passes each element to fn for transformation
end
return result
end
-- -----------------------------------------------
-- Function that returns a function (function factory)
-- -----------------------------------------------
-- Creates and returns a closure that adds n
local function make_adder(n)
return function(x)
return x + n -- n is retained as an upvalue
end
end
local add10 = make_adder(10)
print(add10(5)) -- 15
-- -----------------------------------------------
-- Memoization
-- -----------------------------------------------
-- Creates a closure that caches computation results
local function memoize(fn)
local cache = {} -- the closure holds the cache table as an upvalue
return function(n)
if cache[n] == nil then
cache[n] = fn(n)
end
return cache[n]
end
end
Syntax Reference
| Concept / Pattern | Description |
|---|---|
| Upvalue | A variable captured by a closure from an outer scope. The upvalue is retained as long as the closure remains alive. |
| Closure | A function that has upvalues. In Lua, all functions can potentially be closures. |
| Counter generation | Pattern that returns a counter function holding state via an upvalue. The internal state is updated on each call. |
| Function factory | A function that creates and returns closures with different behavior based on arguments. make_adder and make_multiplier are typical examples. |
| map | A higher-order function that applies a function to each element of a table and returns a new table. Does not modify the original table. |
| filter | A higher-order function that applies a predicate to each element and returns a new table containing only elements for which the predicate returns true. |
| reduce | A higher-order function that accumulates each element of a table into a single value. Used for aggregations such as sum and maximum. |
| Memoization | An optimization pattern that saves computation results in a cache table inside a closure, avoiding redundant computation for the same arguments. |
| Partial application | Pattern that returns a new function with some arguments of the original function fixed. An upvalue holds the fixed values. |
Sample Code
eva_closure.lua
-- eva_closure.lua — closures and higher-order functions sample
-- Uses Evangelion character data to demonstrate
-- closures, map, filter, reduce, and memoization
-- -----------------------------------------------
-- Counter generation (holding state with an upvalue)
-- -----------------------------------------------
-- Creates a counter to manage pilot IDs
local function make_counter(start, label)
local count = start -- state is held as an upvalue
return function()
local current = count
count = count + 1
return label .. current
end
end
local next_pilot_id = make_counter(1, "PILOT-")
print("=== Pilot ID Assignment ===")
print(next_pilot_id()) -- PILOT-1
print(next_pilot_id()) -- PILOT-2
print(next_pilot_id()) -- PILOT-3
print("")
-- -----------------------------------------------
-- Higher-order functions: implementing map / filter / reduce
-- -----------------------------------------------
-- map: applies fn to each element and returns a new table
local function map(tbl, fn)
local result = {}
for i, v in ipairs(tbl) do
result[i] = fn(v)
end
return result
end
-- filter: returns a new table with only elements for which fn returns true
local function filter(tbl, fn)
local result = {}
for _, v in ipairs(tbl) do
if fn(v) then
result[#result + 1] = v
end
end
return result
end
-- reduce: folds tbl starting from init using fn
local function reduce(tbl, fn, init)
local acc = init
for _, v in ipairs(tbl) do
acc = fn(acc, v)
end
return acc
end
-- Pilot sync rate data
local pilots = {
{ name = "Ikari Shinji", sync = 400, unit = "EVA-01" },
{ name = "Ayanami Rei", sync = 380, unit = "EVA-00" },
{ name = "Soryu Asuka Langley", sync = 300, unit = "EVA-02" },
{ name = "Nagisa Kaworu", sync = 999, unit = "EVA-13" },
{ name = "Katsuragi Misato", sync = 0, unit = "none" },
}
-- map: generate a list of names only
local names = map(pilots, function(p) return p.name end)
print("=== Pilot List ===")
for _, name in ipairs(names) do
print(" " .. name)
end
print("")
-- filter: narrow down to pilots with sync rate >= 300
local active = filter(pilots, function(p) return p.sync >= 300 end)
print("=== Sync Rate >= 300 ===")
for _, p in ipairs(active) do
print(string.format(" %s (%s) Sync rate: %d", p.name, p.unit, p.sync))
end
print("")
-- reduce: calculate the total sync rate
local total = reduce(pilots, function(acc, p) return acc + p.sync end, 0)
print("=== Total Sync Rate ===")
print(" Total: " .. total)
print("")
-- -----------------------------------------------
-- Function factory (function that returns a closure)
-- -----------------------------------------------
-- Creates a closure that multiplies sync rate by a factor
local function make_sync_booster(multiplier)
return function(p)
return {
name = p.name,
unit = p.unit,
sync = math.floor(p.sync * multiplier),
}
end
end
local boost_x1_5 = make_sync_booster(1.5) -- 1.5x booster
local boosted = map(active, boost_x1_5)
print("=== After 1.5x Sync Rate Boost ===")
for _, p in ipairs(boosted) do
print(string.format(" %s: %d", p.name, p.sync))
end
print("")
-- -----------------------------------------------
-- Memoization (optimizing recursive functions)
-- -----------------------------------------------
-- Creates a memoization wrapper
local function memoize(fn)
local cache = {} -- the closure holds the cache as an upvalue
return function(n)
if cache[n] == nil then
cache[n] = fn(n)
end
return cache[n]
end
end
-- Memoize a naive recursive Fibonacci function
local fib
fib = memoize(function(n)
if n <= 1 then return n end
return fib(n - 1) + fib(n - 2) -- fib is memoized so this is fast
end)
print("=== Fibonacci (searching up to Nagisa Kaworu's sync rate of 999) ===")
-- Find the largest Fibonacci number <= 999
local best = 0
for i = 0, 16 do
local f = fib(i)
if f <= 999 then best = f end
end
print(" Largest Fibonacci number <= 999: " .. best)
print("")
-- -----------------------------------------------
-- Partial application
-- -----------------------------------------------
-- Returns a closure with the first argument of fn fixed to a
local function partial(fn, a)
return function(b)
return fn(a, b)
end
end
-- Calculates sync score (base + bonus)
local function calc_score(base, bonus)
return base + bonus
end
local shinji_score = partial(calc_score, 400) -- fix Ikari Shinji's base sync rate
print("=== Partial Application: Ikari Shinji Score Calculation ===")
print(" Bonus 50: " .. shinji_score(50)) -- 450
print(" Bonus 100: " .. shinji_score(100)) -- 500
print(" Bonus 200: " .. shinji_score(200)) -- 600
lua eva_closure.lua === Pilot ID Assignment === PILOT-1 PILOT-2 PILOT-3 === Pilot List === Ikari Shinji Ayanami Rei Soryu Asuka Langley Nagisa Kaworu Katsuragi Misato === Sync Rate >= 300 === Ikari Shinji (EVA-01) Sync rate: 400 Ayanami Rei (EVA-00) Sync rate: 380 Soryu Asuka Langley (EVA-02) Sync rate: 300 Nagisa Kaworu (EVA-13) Sync rate: 999 === Total Sync Rate === Total: 2079 === After 1.5x Sync Rate Boost === Ikari Shinji: 600 Ayanami Rei: 570 Soryu Asuka Langley: 450 Nagisa Kaworu: 1498 === Fibonacci (searching up to Nagisa Kaworu's sync rate of 999) === Largest Fibonacci number <= 999: 987 === Partial Application: Ikari Shinji Score Calculation === Bonus 50: 450 Bonus 100: 500 Bonus 200: 600
Common Mistakes
Common Mistake 1: Closures inside loops sharing the loop variable
In Lua, for loop variables have an independent local scope for each iteration, so the classic pitfall (all closures capturing only the final value) does not occur. However, caution is needed when using while loops or manual counters.
ng_example.lua
local funcs = {}
local i = 1
while i <= 3 do
funcs[i] = function() return i end -- captures i directly
i = i + 1
end
for _, f in ipairs(funcs) do
print(f()) -- all print 4 (the value of i after the loop)
end
4 4 4
Copy the loop variable's value into a local variable and capture that instead.
ok_example.lua
local funcs = {}
local i = 1
while i <= 3 do
local captured = i -- copy the current value of i
funcs[i] = function() return captured end
i = i + 1
end
for _, f in ipairs(funcs) do
print(f())
end
1 2 3
Common Mistake 2: Not being aware that multiple closures share the same upvalue
Multiple closures that reference the same outer-scope variable share that variable. When one closure updates the variable, the others are affected too.
ng_example2.lua
local function make_pair()
local count = 0
local inc = function() count = count + 1 end
local get = function() return count end
return inc, get
end
local inc, get = make_pair()
print(get()) -- 0
inc()
inc()
print(get()) -- 2 -- inc and get share count
0 2
This may be intentional, but to prevent unintended sharing, give each closure its own independent variable.
ok_example2.lua
local function make_counter()
local count = 0
return {
inc = function() count = count + 1 end,
get = function() return count end,
reset = function() count = 0 end,
}
end
local c = make_counter()
c.inc()
c.inc()
print(c.get()) -- 2
c.reset()
print(c.get()) -- 0
2 0
Overview
In Lua, functions are first-class values and can be freely assigned to variables, passed as arguments, and used as return values. A closure is a function that captures variables from an outer scope (upvalues). Because upvalues are not garbage-collected as long as the closure remains alive, they are useful for implementing stateful counters and caches.
Higher-order functions are functions that take a function as an argument or return a function. Implementing map, filter, and reduce yourself allows table operations to be written declaratively. Lua's standard library does not include these, but they can be implemented in a few dozen lines.
Function factories generate closures with different behavior based on arguments, as in make_adder or make_sync_booster. Memoization caches computation results in a table inside a closure, significantly reducing redundant computation in recursive cases like Fibonacci. Partial application generates a specialized function from a general one by holding fixed arguments as upvalues. For table fundamentals, see Table Basics; for basic function definitions, see Function Basics.
If you find any errors or copyright issues, please contact us.