Skip to content

More about functions

Lesson 5.2

Functions that take any number of arguments, functions that remember values, and functions that call themselves.

Any number of arguments

Three dots ... as the last parameter accept any number of extra arguments. They are called varargs:

local function total(...)
  local sum = 0
  for _, n in ipairs({ ... }) do
    sum = sum + n
  end
  return sum
end

print(total(1, 2, 3))      --> 6
print(total(10, 20, 30, 40)) --> 100
print(total())             --> 0

{ ... } puts the arguments into a table so you can loop over them. The _ is just a variable name; by habit it means “I don’t need this value”.

You can also pass ... straight on to another function, and mix it with normal parameters:

local function log(level, ...)
  print("[" .. level .. "]", ...)
end

log("INFO", "Player joined:", "Alex")  --> [INFO]	Player joined:	Alex

When an argument can be nil

{ ... } has a problem with nil in the middle: the length of a table with holes is unreliable. Two tools handle every case:

  • select("#", ...) gives the real number of arguments, nils included.
  • select(n, ...) gives the arguments from position n onwards.
  • table.pack(...) makes a table and stores the count in the field n.
local function count(...)
  return select("#", ...)
end
print(count(1, nil, 3))  --> 3

local args = table.pack("a", nil, "c")
print(args.n, args[3])   --> 3	c

print(select(2, "x", "y", "z"))  --> y	z

table.unpack does the opposite of table.pack: it turns a list back into separate values.

local pos = { 100, 64, -30 }
print(table.unpack(pos))  --> 100	64	-30

Closures

A function can use local variables from the code around it. It keeps them alive, even after that code has finished. A function together with the variables it remembers is called a closure:

local function make_counter()
  local count = 0
  return function()
    count = count + 1
    return count
  end
end

local next_id = make_counter()
print(next_id())  --> 1
print(next_id())  --> 2
print(next_id())  --> 3

local other = make_counter()
print(other())    --> 1

Each call to make_counter creates a fresh count, so next_id and other count separately. Nobody else can touch count: it is private to the closure.

Closures are everywhere in event-driven code. A handler function can remember settings or state from where it was created:

local function make_cooldown(seconds)
  local last = -math.huge
  return function(now)
    if now - last < seconds then
      return false
    end
    last = now
    return true
  end
end

local can_use = make_cooldown(5)
print(can_use(0))  --> true
print(can_use(3))  --> false
print(can_use(6))  --> true

Recursion

A function can call itself. This is called recursion. Each call must move closer to a case that stops:

local function factorial(n)
  if n <= 1 then
    return 1
  end
  return n * factorial(n - 1)
end

print(factorial(5))  --> 120

Recursion fits naturally when data contains smaller data of the same kind, such as a folder with subfolders or a crafting recipe whose ingredients have their own recipes.

A local function can call itself because local function f declares f before the body. The longer form local f = function() ... f() ... end would not work: inside the body, f would still be the global f, which is nil.

Stack overflow

Every call that hasn’t returned yet takes some memory. Recursion without a stopping case ends with the error stack overflow. A loop is often the safer choice for very long sequences.

One special case: when a function ends with return f(...), Lua reuses the current call instead of stacking a new one. This is a tail call, and it can recurse forever without overflowing.

Functions in tables

Functions are often stored in tables, grouped by topic. You will see string.upper, math.floor, and in Celestial cl.items.register. Each is a function stored in a table. The Tables chapter shows how to build such tables, and Classes and inheritance how to give them methods.

Try it

Write make_multiplier(factor) that returns a function. The returned function takes a number and multiplies it by factor. Then write sum_all(...) that adds all its arguments and ignores any that aren’t numbers (use select so nils don’t break it).

Show a solution
local function make_multiplier(factor)
  return function(x)
    return x * factor
  end
end

local triple = make_multiplier(3)
print(triple(7))  --> 21

local function sum_all(...)
  local sum = 0
  for i = 1, select("#", ...) do
    local v = select(i, ...)
    if type(v) == "number" then
      sum = sum + v
    end
  end
  return sum
end

print(sum_all(1, nil, "x", 4))  --> 5

Recap: ... takes any number of arguments, closures remember the variables around them, and recursion solves a problem by solving a smaller copy of it.