Lua from the ground up

Functions

Naming a block of work with local function, giving values back, returning more than one thing, taking a variable number of arguments, and defaulting a missing one.

Part 8 of 12

Intermediate17 min readBack to Courses

Naming work

A function is a block of work with a name. local function greet(name) ... end defines one, and calling it by that name runs it. The names in the brackets are the parameters; the values you pass in are the arguments.

A return hands a value back to the caller and ends the function right there. A function with no return gives back nothing, which is fine when its job is to print or to change something.

More than one answer

A function can return several values at once: return width, height. The caller receives them in order, so two names on the left catch both. This is a Lua habit worth leaning on, because it saves building a table just to carry two numbers.

When a call sits last in a list, its extra returns are passed along. This is why the print function can take any number of values at once and show them all.

Varargs, and defaults

A parameter written as three dots collects any number of extra arguments. Inside the function those dots stand for them, and wrapping the dots in curly braces turns them into a table you can walk with ipairs.

Lua has no default values in the signature, but the idiom is one line: h = h or 1 uses the value when it was given and falls back when it was not, because a missing argument arrives as nil.

The call, and the stack

Calling a function pauses the caller and starts a new frame for the callee. Parameters are locals of that frame, filled with the arguments in order. When the function returns, its frame disappears and the caller carries on with the values it was handed.

Arguments are copied into those parameters. For a number or a string that means the function cannot change the caller's value. For a table the copy is the reference: the function can change the table's fields, but assigning a new table to the parameter only changes what that local name points at.

How many values come back

A return list can be longer or shorter than the names waiting for it. Extra values are dropped, and missing ones arrive as nil. A call in the middle of a list is cut to one value; only a call in the last position expands to all of them.

Parentheses force a call back to one value. print((f())) shows only the first return, which is exactly what you want when a function returns a value and a status but you only care about one of them. select("#", ...) counts varargs without turning them into a table.

Tail calls

A call in the return position is a tail call: return f(x) hands the caller’s frame straight to f instead of stacking a new one. Lua turns that into a jump, so a tail-recursive loop can run for a million steps in constant stack space. Wrapping the call, as in return f(x) + 1, is no longer a tail call and does grow the stack.

That distinction is the practical reason to write a recursive helper so its recursive call is the whole return value. An accumulator parameter carries the work forward, and the base case returns it. The shape is a loop written as a function, and Lua runs it like one.

Unpacking a list into arguments

table.unpack(list) does the reverse of {...}: it turns a list into a series of arguments. print(table.unpack({1, 2, 3})) is the same as print(1, 2, 3). In the last position it expands to every element, just as a multi-return call does.

That makes it the bridge between a function that takes varargs and a function that takes a list. math.max(table.unpack(scores)) asks for the largest score without writing a loop. The list has to be dense and not too long, since every element becomes an argument on the stack.

Functions that give things back

local function area(w, h)
  h = h or 1
  return w * h
end

print(area(5))
print(area(5, 2))

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

print(total(1, 2, 3, 4))

Three function mistakes. Forgetting return, so the caller silently gets nil. Writing a recursive function as local f = function() ... f() ... end: the name is not in scope inside its own body, so it calls a global that is nil — use local function f() instead. And handing back a table you keep changing, which surprises a caller that expected a snapshot.

Try these next

  • Write a function that returns a number and a string, and catch both.
  • Write a function that takes any number of numbers and returns their average, using select("#", ...).
  • Change a table inside a function and print it outside; then assign a new table to the parameter and see what the caller still sees.

Two answers and a fallback

Run it and watch the fallback fire on the first area call. Then write a function that returns three values at once, and one that sums any number of arguments you give it.

Output
Click "Run" to execute your code

Smallest, largest, and a count

Run it, then call bounds with a different set of numbers. Change mean so it returns both the total and the average.

Output
Click "Run" to execute your code

Give each function one job and a name that says what that job is. A short function is easy to check by calling it and printing the result, which is exactly how the examples in this course test themselves.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

How do you define a function that stays local?