Polyglot
Languages Lua functions
Lua § functions

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 function by default.
  • Use multiple returns — admit substantial named returns.
  • Use ... for varargs; select for counting/access.
  • Use the colon syntax for methods.
  • Use closures for state encapsulation.
  • Use or for default arguments (with false caveat).
  • Use table-as-argument for substantial named parameters.
  • Use anonymous functions in higher-order calls.
  • Use tail calls for substantial recursion.
  • Return nil, error for expected failures.
  • Use error for 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.