Intermediate

Functions in Lua

Learn how to define and call functions in Lua, including multiple return values, varargs, closures, and recursion with Docker-ready examples

Functions are the primary unit of reuse in Lua, and they are also one of the language’s most powerful features. In Lua, functions are first-class values: you can store them in variables, pass them as arguments, return them from other functions, and keep them in tables. This is what allows Lua to support a functional style of programming on top of its procedural core.

Lua keeps the syntax minimal—there is one keyword, function, and one way to return values, return. But beneath that simplicity sit features that many older languages lack: a function can return multiple values at once, accept a variable number of arguments, and capture surrounding variables in a closure. Because Lua is dynamically typed, parameters have no declared types, and the language has no built-in default-parameter syntax—idioms fill that gap instead.

In this tutorial you’ll learn how to define and call functions, work with parameters and return values, control variable scope, write recursive functions, and use higher-order functions and closures. Each example is a self-contained file you can run with Docker.

Defining and Calling Functions

A function is defined with the function keyword. Prefixing the definition with local keeps it scoped to the current chunk—the recommended default. Lua functions can return more than one value, which is idiomatic rather than exotic.

Create a file named functions.lua:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-- A simple function with one parameter
local function greet(name)
    return "Hello, " .. name .. "!"
end

print(greet("Lua"))

-- Multiple parameters
local function add(a, b)
    return a + b
end

print(add(3, 4))

-- Multiple return values are a first-class Lua feature
local function minmax(a, b)
    if a < b then
        return a, b
    else
        return b, a
    end
end

local lo, hi = minmax(10, 3)
print("min =", lo, "max =", hi)

The minmax function returns two values, and the assignment local lo, hi = minmax(10, 3) unpacks them into two variables. No tuples or arrays are needed.

Default Arguments and Varargs

Lua has no dedicated syntax for default parameter values. The common idiom uses the or operator: if an argument is nil (omitted), or falls back to the default. For functions that accept any number of arguments, Lua provides the ... (vararg) expression.

Create a file named defaults.lua:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- The `or` idiom supplies a default when an argument is omitted
local function greet(name, greeting)
    greeting = greeting or "Hello"
    return greeting .. ", " .. name .. "!"
end

print(greet("Ada"))
print(greet("Ada", "Welcome"))

-- Variadic functions collect extra arguments with ...
local function sum(...)
    local total = 0
    for _, n in ipairs({...}) do
        total = total + n
    end
    return total
end

print(sum(1, 2, 3, 4, 5))

-- select("#", ...) counts how many arguments were passed
local function count(...)
    return select("#", ...)
end

print(count("a", "b", "c"))

Inside a variadic function, {...} packs the arguments into a table you can iterate, and select("#", ...) reports how many were supplied.

Variable Scope: Local vs Global

By default, an assignment that is not marked local creates a global variable. Globals are visible everywhere and are easy to create by accident, so idiomatic Lua uses local for almost everything. Variables declared local inside a function disappear once the function returns.

Create a file named scope.lua:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
local x = 10  -- local to this chunk

local function show()
    local y = 20      -- local to this function only
    counter = 100     -- no `local` keyword, so this is GLOBAL
    print("inside: x =", x, "y =", y)
end

show()
print("outside: x =", x)
print("global counter =", counter)

-- y was local to show(), so it is nil out here
print("y outside =", y)

Notice that x is visible inside show() because the function is defined in the same chunk, while y is invisible outside the function. The unmarked counter leaks into the global scope—a reminder to reach for local deliberately.

Recursion

A function can call itself. Because a local function name is in scope within its own body, recursion works naturally. Classic examples are factorial and the Fibonacci sequence.

Create a file named recursion.lua:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- Factorial: n! = n * (n-1) * ... * 1
local function factorial(n)
    if n <= 1 then
        return 1
    end
    return n * factorial(n - 1)
end

print("5! =", factorial(5))
print("10! =", factorial(10))

-- Fibonacci sequence via recursion
local function fib(n)
    if n < 2 then
        return n
    end
    return fib(n - 1) + fib(n - 2)
end

for i = 0, 9 do
    io.write(fib(i), " ")
end
print()

The loop uses io.write, which prints without adding spaces or a newline, so the Fibonacci numbers appear on one line separated by a single space. The trailing print() adds the final newline.

Higher-Order Functions and Closures

Because functions are values, you can pass them to other functions and return them from functions. A function that takes or returns another function is a higher-order function. When a returned function captures a local variable from its defining scope, that captured state is a closure.

Create a file named higher_order.lua:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-- A higher-order function: it receives a function as an argument
local function apply(fn, value)
    return fn(value)
end

local function square(n)
    return n * n
end

print(apply(square, 6))

-- Anonymous functions can be passed inline
print(apply(function(n) return n + 1 end, 41))

-- A closure: make_counter returns a function that remembers `count`
local function make_counter()
    local count = 0
    return function()
        count = count + 1
        return count
    end
end

local next_id = make_counter()
print(next_id())
print(next_id())
print(next_id())

-- Each counter keeps its own independent state
local other = make_counter()
print(other())

Each call to make_counter creates a fresh count variable. The inner function captures that variable, so next_id and other count independently—the essence of a closure.

Running with Docker

1
2
3
4
5
6
7
8
9
# Pull the official image
docker pull nickblah/lua:5.4-alpine

# Run each example
docker run --rm -v $(pwd):/app -w /app nickblah/lua:5.4-alpine lua functions.lua
docker run --rm -v $(pwd):/app -w /app nickblah/lua:5.4-alpine lua defaults.lua
docker run --rm -v $(pwd):/app -w /app nickblah/lua:5.4-alpine lua scope.lua
docker run --rm -v $(pwd):/app -w /app nickblah/lua:5.4-alpine lua recursion.lua
docker run --rm -v $(pwd):/app -w /app nickblah/lua:5.4-alpine lua higher_order.lua

Expected Output

Running functions.lua:

Hello, Lua!
7
min =	3	max =	10

Running defaults.lua:

Hello, Ada!
Welcome, Ada!
15
3

Running scope.lua:

inside: x =	10	y =	20
outside: x =	10
global counter =	100
y outside =	nil

Running recursion.lua:

5! =	120
10! =	3628800
0 1 1 2 3 5 8 13 21 34 

Running higher_order.lua:

36
42
1
2
3
1

Key Concepts

  • Functions are first-class values — they can be stored in variables, passed as arguments, returned from other functions, and held in tables.
  • Multiple return values are built in — return a, b and local x, y = f() unpack naturally without tuples or arrays.
  • No default-parameter syntax — use the name = name or default idiom, since an omitted argument arrives as nil.
  • Varargs with ... — collect extra arguments with {...} and count them with select("#", ...).
  • local controls scope — unmarked assignments create global variables, so prefer local for functions and variables to avoid accidental globals.
  • Recursion is straightforward — a local function’s name is in scope inside its own body, so it can call itself directly.
  • Closures capture local state — a function returned from another function remembers the locals it referenced, each instance keeping independent state.

Next Steps

Continue to I/O Operations to learn how Lua reads input, writes formatted output, and works with files.

Running Today

All examples can be run using Docker:

docker pull nickblah/lua:5.4-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining