Learn Lua in Neovim: Multiple Returns and Variadics — return a, b and ... | Episode 13
Video: Learn Lua in Neovim: Multiple Returns and Variadics — return a, b and ... | Episode 13 by Taught by Celeste AI - AI Coding Coach
Lua Multiple Returns and Variadics: a, b and ...
return a, bto return two values.local x, y = f()to receive both.function f(...)for variable-arity.select("#", ...)for the count,{...}to convert into a table.
Lua functions can return more than one value, and accept any number of arguments. Both features feel unusual coming from C-like languages — and both make a lot of stdlib functions cleaner.
Returning multiple values
function min_max(a, b, c)
local mn = math.min(a, b, c)
local mx = math.max(a, b, c)
return mn, mx
end
local smallest, largest = min_max(5, 2, 8)
print("Min: " .. smallest .. ", Max: " .. largest)
Output:
Min: 2, Max: 8
return mn, mx returns two values. The caller uses local smallest, largest = ... to capture both.
This isn't a tuple; it's two distinct values that the language tracks separately. You don't need to construct anything to "pack" them.
A common case: division and remainder
function divide(a, b)
return a // b, a % b
end
local q, r = divide(17, 5)
print("17 / 5 = " .. q .. " remainder " .. r)
Output:
17 / 5 = 3 remainder 2
Quotient and remainder in one call. Many Lua stdlib functions return multiple values (string.find, io.read, pcall).
Discarding with _
local _, remainder = divide(100, 7)
print("Remainder of 100/7: " .. remainder)
_ is a conventional throwaway name — assign the unwanted value to it and ignore. Lua doesn't treat _ specially; it's just a normal local variable that you (and the reader) agree not to use.
Multiple returns in the middle disappear
This is the tricky part:
function pair() return 1, 2 end
local a, b, c = pair(), 99
-- a = 1, b = 99, c = nil
When a multi-return call appears anywhere except the last position, only its first value is used. The 2 from pair() gets thrown away because there's another expression after it.
When it's last, all values are collected:
local a, b, c = 99, pair()
-- a = 99, b = 1, c = 2
This rule applies to function calls and ... (next section). It catches everyone the first time.
Using multi-return in print
print(divide(17, 5)) -- prints: 3 2 (tab-separated)
print(divide(17, 5), 99) -- prints: 3 99 (only first value of divide)
print accepts variadic args. When divide(...) is the last argument, both its values are passed; otherwise only the first.
Variadic functions: ...
function sum(...)
local total = 0
local count = select("#", ...)
local args = {...}
for i = 1, count do
total = total + args[i]
end
return total, count
end
local result, n = sum(1, 2, 3, 4, 5)
print("Sum of " .. n .. " numbers: " .. result)
local result2, n2 = sum(10, 20)
print("Sum of " .. n2 .. " numbers: " .. result2)
Output:
Sum of 5 numbers: 15
Sum of 2 numbers: 30
function sum(...) declares a variadic — accepts any number of arguments. Inside the body, ... is a magic expression that expands to all the arguments.
Three common ways to work with ...:
- Pack into a table:
local args = {...}collects them.args[1],args[2], etc. - Count them:
select("#", ...)returns the count, includingnils. - Forward them: call
another_fn(...)to pass through.
Why select("#", ...) and not #args?
local args = {1, 2, nil, 4}
print(#args) -- 4 (sometimes 2 — undefined!)
print(select("#", ...)) -- always correct count of args passed in
Lua's # operator on a table with embedded nils is undefined — it might return any boundary. select("#", ...) reports the actual number of arguments, including nil slots.
For all-non-nil arguments, #args works. For safety with possibly-nil arguments, use select.
Mixing fixed and variadic params
function log(level, ...)
local msg = string.format(...)
print("[" .. level .. "] " .. msg)
end
log("INFO", "User %s logged in at %d", "Alice", 1234)
level is the fixed first parameter. ... captures everything else. We forward ... into string.format.
The variadic must be last. function log(..., level) is a syntax error.
select for picking specific args
function third(...)
return select(3, ...) -- everything from arg 3 onwards
end
print(third("a", "b", "c", "d")) -- "c", "d"
select(n, ...) returns args n, n+1, n+2, ... — useful when you want to skip the front.
Forwarding a multi-return chain
function safe_call(fn, ...)
return pcall(fn, ...)
end
pcall(fn, args...) calls fn with the args, returning true/false plus whatever fn returned. We pass ... straight through. This is the canonical "wrap a call with error handling" pattern (covered in episode 27).
Common stumbles
Multi-return swallowed mid-expression. f(g(), h()) — only the first value of g() is used. Position matters.
Forgetting select("#", ...) for variadic length. If args might include nil, #{...} is unreliable.
Using ... outside a variadic function. ... is only valid in the body of a function declared with (...). At the top level, it refers to the script's command-line arguments.
Receiving more or fewer values than returned. Extras are nil; not-asked-for ones are dropped silently. No error.
Returning a value when you meant to print it. Make sure the caller actually does something with the returned values.
What's next
Episode 14: scope. The local keyword, block scope (if, for create their own scope), shadowing, and why "always use local" is the strongest Lua tip.
Recap
return a, b, c for multiple returns; local x, y = f() to receive. Multi-return only fully expands in the last position. function f(...) for variadics; inside, {...} packs into a table, select("#", ...) counts (handles nils). Mix fixed and variadic with the variadic last: function log(level, ...). Forward with another_fn(...).
Next episode: scope.