Skip to content

5. Functions

Learning Objectives

  • be familiar with Lua functions
  • understand how to use functions as a tool for abstraction and deduplication
  • understand the uses of higher order functions
  • know when to use closures to represent stateful functions

Functions

Functions in Lua are the main tool for forming abstractions. We define functions similarly to other programming languages.

-- function foo which takes two parameters and returns their sum
function foo(x, y)
    return x + y
end

Functions are really useful as abstractions over complex processes. By writing good functions with clear documentation, we allow other programmers to use our function without needing to understand the internals of the implementation.

Functions also allow us to consolidate repeated code into a single unit which can be modified once and have its changed replicated for every instance where its called.

Note that, unlike some other languages, Lua does not support function overloading.

Common mistake

Lua does not throw any errors when we pass less or more arguments than necessary. Suppose we have the previous function foo(x, y) then we have this mapping from arguments to parameters:

CALL             PARAMETERS
f(3)             x=3, y=nil
f(3, 4)          x=3, y=4
f(3, 4, 5)       x=3, y=4   (5 is discarded)
This can lead to runtime errors if not careful.

Default arguments

We can use the Lua's nonstrict argument checking to provide default arguments to functions.

function foo(a, b)
    a = a or 1  -- default value for a is 1
    b = b or -1 -- default value for b is -1
    return a + b
end

Multiple results

Functions can also return multiple results which can be stored in a multi-assignment statement.

function bar(a, b)
    return a+1, b+1
end
local c, d = bar(1, 2)  -- c will be 2, d will be 3
Similarly to variable arguments, excess variables will have the value nil

Since functions are considered first-class citizens in Lua, we can do a lot of interesting things with them.

Callbacks

Callbacks are functions passed as arguments to another function which we call a higher order function.

For instance, we can have the higher-order function compute which takes in numbers x, y and a binary function we call operation and returns operation(x, y) whatever the operation may be:

-- higher order function
function compute(x, y, operation)
    return operation(x, y)
end

-- callbacks
local add = function(a, b) return a + b end
local mult = function(a, b) return a * b end

-- call compute with different callbacks
print(compute(5, 10, add))      -- output: 15
print(compute(5, 10, mult))     -- output: 50

We can use this pattern to design many more flexible and powerful functions.

Anonymous functions

Functions can also be anonymous, meaning that they do not have to be declared with a name. A very common usecase for anonymous functions is to pass them to higher-order functions as callbacks without having to name them.

-- we pass in an anonymous subtraction function as a callback
print(compute(5, 10, function (a, b) return a - b end))     -- output: -5

Naming anonymous functions

While less common, we can also name anonymous functions by assigning them to a variable. We usually do this when we are returning a function.

-- naming an anonymous function
local add = function(a, b) return a + b end
-- naming any function returned by another function
local randomFn = createRandomFunction()

Closures

Lua support closures, which are functions which capture and "remember" the environment in which they were created. Closures are particularly useful for representing stateful functions such as a counter.

function createCounter(start)
    local count = start or 0    -- let 0 be the default counter start value
    return function()           -- return a function with count variable captured
        count = count + 1
        return count
    end
end

local counter = createCounter(20)   -- create a counter starting at 20
print(counter())                    -- output: 21, the state has changed
print(counter())                    -- output: 22, the state has changed again

Local functions

Interestingly, functions can also be local which means that they follow the same scoping rules as local variables.

For instance, if we declare a local function inside of a function, the local function is not accessible from outside.

function outer()
    local function inner() end

    inner()     -- works
end

inner()         -- doesn't work

This is of course useful to prevent inner functions like helper functions from polluting the namespace.