Functions and closures
Lua functions are first-class values — admit assignment, passing as arguments, returning, and substantial closure over variables. Functions admit multiple return values (first-class), variadic parameters (...), anonymous form (function() ... end), and implicit self parameter via colon syntax (obj:method()). Closures admit substantial state encapsulation through captured upvalues. The combination — first-class functions, multiple returns, varargs, the colon syntax for methods, the closure-based state encapsulation — is the substance of Lua’s function surface.
Function declarations
Three principal forms:
-- Global function (rare; conventionally avoided):
function greet(name)
return "Hello, " .. name
end
-- Local function (conventional):
local function greet(name)
return "Hello, " .. name
end
-- As a value:
local greet = function(name)
return "Hello, " .. name
end
The local function form is conventionally preferred — admits substantial recursion via the binding being in scope inside the body.
Multiple return values
A distinctive Lua feature:
local function divmod(a, b)
return a // b, a % b
end
local q, r = divmod(17, 5)
print(q, r) -- 3, 2
-- Discarding values:
local q = divmod(17, 5) -- only first
local _, r = divmod(17, 5) -- skip first
-- More than the function returns:
local q, r, extra = divmod(17, 5) -- extra is nil
The mechanism admits substantial conciseness for “named return values” patterns:
local function find(t, target)
for i, v in ipairs(t) do
if v == target then return i, v end
end
return nil -- not found
end
local idx, value = find({10, 20, 30}, 20)
if idx then
print("at", idx, ":", value)
end
Variadic functions
The ... admits variable arguments:
local function sum(...)
local total = 0
for _, v in ipairs({...}) do
total = total + v
end
return total
end
print(sum(1, 2, 3)) -- 6
print(sum()) -- 0
print(sum(1, 2, 3, 4, 5)) -- 15
The ... represents the variadic arguments; {...} packs them into a table.
For forwarding:
local function wrapper(...)
log("called with", ...) -- forward to log
return underlying(...) -- forward to underlying
end
For select:
local function process(first, ...)
print("first:", first)
print("count:", select("#", ...)) -- count of remaining
print("second:", select(2, ...)) -- second value
end
process("a", "b", "c", "d")
-- first: a
-- count: 3
-- second: c
The select admits substantial varargs manipulation.
Anonymous functions
local function square(n)
return n * n
end
-- Equivalent:
local square = function(n)
return n * n
end
-- Inline (anonymous):
table.sort(arr, function(a, b) return a < b end)
local doubled = map(arr, function(x) return x * 2 end)
The anonymous form is conventional in higher-order calls.
Closures
Functions admit closures over enclosing-scope locals:
local function make_counter()
local count = 0
return function()
count = count + 1
return count
end
end
local counter = make_counter()
print(counter()) -- 1
print(counter()) -- 2
print(counter()) -- 3
Each call to make_counter produces a new closure with its own count. The captured variables are called upvalues.
For shared state:
local function make_pair()
local value = 0
return function() value = value + 1 return value end,
function() return value end
end
local incr, get = make_pair()
incr()
incr()
print(get()) -- 2
Methods (colon syntax)
The : admits implicit self parameter:
local Person = {}
Person.__index = Person
function Person.new(name)
return setmetatable({name = name}, Person)
end
function Person:greet(other) -- colon: implicit self
return self.name .. " greets " .. other
end
-- Equivalent without colon:
function Person.greet(self, other)
return self.name .. " greets " .. other
end
local p = Person.new("Alice")
print(p:greet("Bob")) -- "Alice greets Bob"
print(p.greet(p, "Bob")) -- equivalent
Treated in OOP idioms.
Function as value
Functions are first-class — admit substantial higher-order patterns:
-- As argument:
local function apply(fn, x)
return fn(x)
end
print(apply(function(x) return x * 2 end, 5)) -- 10
-- As return value:
local function adder(n)
return function(x) return x + n end
end
local add5 = adder(5)
print(add5(3)) -- 8
-- In table:
local handlers = {
click = function(e) print("clicked") end,
keypress = function(e) print("key:", e.key) end
}
handlers.click({})
Default values via or
Lua does not admit default arguments natively; the conventional substitute uses or:
local function greet(name, greeting)
name = name or "world"
greeting = greeting or "Hello"
return greeting .. ", " .. name
end
greet() -- "Hello, world"
greet("Alice") -- "Hello, Alice"
greet("Alice", "Hi") -- "Hi, Alice"
The pitfall: if name is false, the default is used:
local function f(x)
x = x or "default"
-- ...
end
f(false) -- x = "default" (false treated as falsy)
-- Defence:
local function f(x)
if x == nil then x = "default" end
-- ...
end
Named arguments via table
For substantial parameter lists, the conventional pattern uses a table:
local function fetch(opts)
local url = opts.url or error("url required")
local method = opts.method or "GET"
local headers = opts.headers or {}
local timeout = opts.timeout or 30
-- ...
end
fetch{
url = "https://example.com",
method = "POST",
headers = {Authorization = "Bearer ..."}
}
The table-as-argument syntax fetch{ ... } is sugar for fetch({ ... }) — admit substantial named-argument style.
Recursion
Functions admit recursion:
local function factorial(n)
if n <= 1 then return 1 end
return n * factorial(n - 1)
end
local function fib(n)
if n < 2 then return n end
return fib(n - 1) + fib(n - 2)
end
For mutual recursion, declare both forward:
local odd, even
odd = function(n)
if n == 0 then return false end
return even(n - 1)
end
even = function(n)
if n == 0 then return true end
return odd(n - 1)
end
print(even(10)) -- true
Lua admits tail-call optimisation — admit substantial recursion without stack growth:
local function loop(n)
if n == 0 then return "done" end
return loop(n - 1) -- tail call (no stack growth)
end
loop(1000000) -- works fine
Common patterns
Higher-order: map
local function map(t, fn)
local result = {}
for i, v in ipairs(t) do
result[i] = fn(v)
end
return result
end
local doubled = map({1, 2, 3}, function(x) return x * 2 end)
-- {2, 4, 6}
Higher-order: filter
local function filter(t, pred)
local result = {}
for _, v in ipairs(t) do
if pred(v) then result[#result + 1] = v end
end
return result
end
local evens = filter({1, 2, 3, 4, 5}, function(x) return x % 2 == 0 end)
-- {2, 4}
Higher-order: reduce
local function reduce(t, init, fn)
local acc = init
for _, v in ipairs(t) do
acc = fn(acc, v)
end
return acc
end
local sum = reduce({1, 2, 3, 4}, 0, function(a, b) return a + b end)
-- 10
Memoization
local function memoize(f)
local cache = {}
return function(x)
if cache[x] == nil then
cache[x] = f(x)
end
return cache[x]
end
end
local fib = memoize(function(n)
if n < 2 then return n end
return fib(n - 1) + fib(n - 2)
end)
Curry
local function curry(f)
return function(a)
return function(b)
return f(a, b)
end
end
end
local add = function(a, b) return a + b end
local curried = curry(add)
print(curried(3)(4)) -- 7
Composition
local function compose(f, g)
return function(x) return f(g(x)) end
end
local inc = function(n) return n + 1 end
local double = function(n) return n * 2 end
local f = compose(inc, double)
print(f(5)) -- 11 (5*2 + 1)
Partial application
local function partial(f, ...)
local args = {...}
return function(...)
local all = {table.unpack(args)}
for _, v in ipairs({...}) do
all[#all + 1] = v
end
return f(table.unpack(all))
end
end
local add = function(a, b, c) return a + b + c end
local add5and10 = partial(add, 5, 10)
print(add5and10(3)) -- 18
Optional callback
local function process(items, on_progress)
for i, item in ipairs(items) do
do_work(item)
if on_progress then
on_progress(i, #items)
end
end
end
process(items) -- no callback
process(items, function(i, n) print(i .. "/" .. n) end)
Multiple return forwarding
local function wrapper(...)
log("calling")
return inner(...) -- forwards multiple returns
end
-- vs single-return wrapper (drops extra):
local function wrapper(...)
log("calling")
local result = inner(...) -- drops extra returns
return result
end
Returning from a try-pattern
local function tryParse(s)
local n = tonumber(s)
if n then return n end
return nil, "not a number"
end
local n, err = tryParse("abc")
if n then
print(n)
else
print("error:", err)
end
Error vs nil-error pattern
Lua admits two error-reporting conventions:
- Throw via
error— substantial for unrecoverable errors. - Return
nil, message— substantial for expected failures.
-- Throwing:
local function divide(a, b)
if b == 0 then error("division by zero") end
return a / b
end
-- Nil-error:
local function divide(a, b)
if b == 0 then return nil, "division by zero" end
return a / b
end
The conventional discipline: nil, message for expected failures (admit caller to handle); error for substantial bugs. Treated in Error handling.
Variadic function
local function log_all(level, ...)
local args = {...}
print("[" .. level .. "]", table.concat({...}, " "))
end
log_all("INFO", "user", "logged", "in")
-- "[INFO] user logged in"
Argument forwarding
local function with_logging(fn)
return function(...)
print("called with", ...)
return fn(...)
end
end
local logged_add = with_logging(function(a, b) return a + b end)
logged_add(3, 4)
-- "called with 3 4"
-- 7
Method chaining
local Builder = {}
Builder.__index = Builder
function Builder.new()
return setmetatable({parts = {}}, Builder)
end
function Builder:add(part)
self.parts[#self.parts + 1] = part
return self -- return self for chaining
end
function Builder:build()
return table.concat(self.parts, ", ")
end
local s = Builder.new():add("a"):add("b"):add("c"):build()
-- "a, b, c"
Closure for state
local function make_counter(start, step)
start = start or 0
step = step or 1
return function()
start = start + step
return start
end
end
local c = make_counter()
print(c()) -- 1
print(c()) -- 2
local c2 = make_counter(100, 10)
print(c2()) -- 110
print(c2()) -- 120
select for varargs
local function process(...)
local n = select("#", ...) -- count of args
print("got", n, "arguments")
for i = 1, n do
local arg = select(i, ...)
print(i, arg)
end
end
process("a", nil, "c")
-- got 3 arguments
-- 1, a
-- 2, nil
-- 3, c
The select("#", ...) is conventional for counting (handles nil correctly, unlike #{...}).
Function as table key
local f = function() return 42 end
local handlers = {[f] = "registered"}
print(handlers[f]) -- "registered"
Lazy evaluation
local function lazy(f)
local computed = false
local value
return function()
if not computed then
value = f()
computed = true
end
return value
end
end
local expensive = lazy(function()
print("computing...")
return 42
end)
print(expensive()) -- "computing..."; 42
print(expensive()) -- 42 (cached)
A note on tail calls
Lua admits proper tail-call optimisation:
-- This is a tail call (no stack growth):
local function loop(n)
if n == 0 then return "done" end
return loop(n - 1)
end
loop(1000000) -- works fine
-- This is NOT a tail call (stack grows):
local function bad(n)
if n == 0 then return "done" end
return bad(n - 1) + 0 -- + 0 prevents tail call
end
bad(1000000) -- stack overflow
The mechanism admits substantial recursion-as-iteration patterns.
A note on the conventional discipline
The contemporary Lua function advice:
- Use
local functionby default. - Use multiple returns — admit substantial named returns.
- Use
...for varargs;selectfor counting/access. - Use the colon syntax for methods.
- Use closures for state encapsulation.
- Use
orfor default arguments (withfalsecaveat). - Use table-as-argument for substantial named parameters.
- Use anonymous functions in higher-order calls.
- Use tail calls for substantial recursion.
- Return
nil, errorfor expected failures. - Use
errorfor substantial bugs.
The combination — first-class functions, multiple returns, varargs via ..., the colon syntax for methods, the closure-based state encapsulation, the tail-call optimisation, the absence of native default arguments — is the substance of Lua’s function surface. The discipline produces concise, expressive functional code with substantial flexibility through closures and multiple returns.