I really just wanted to say “variadic”. That’s the official term for functions with a variable number of arguments. Let’s see how those work in SLua.
For today’s work, I started with my standard little testing framework, and I left in one test from last time, removing the others. I left the one in so that I could be sure that my tests are being run every time I save the file. Sometimes I mess up and they aren’t run, and that can lead to trouble. Anyway we’re good to go.
When I write these little articles, I try to test every bit of code before I paste it into the article. That gives me a decent chance that it works. And when I’m working in my best fashion, I put tests into whatever real script I’m working on. I don’t always work in my best fashion: sometimes I get impatient and just make changes. Sometimes I get away with that.
Variadic. Very ADD ic. It’s fun to say. And it’s a powerful capability for some purposes. The idea is simple: it allows us to write a function with a variable number of input arguments. Why might we want such a thing? Good question, and in my view we rarely do. But a common example is the Lua print
statement, which can print any number of arguments. Let’s do a new function, line_print, that prints each argument on its own line of output.
One simple way to do that would be to provide a list to the function, like this:
function state_entry()
run_tests()
line_print({"hello", "janet", "how are you?"})
end
function line_print(list)
for k,v in list do
print(k, v)
end
end
And that works just fine:
[06:19] JR20250502 Variadic: 1 hello
[06:19] JR20250502 Variadic: 2 janet
[06:19] JR20250502 Variadic: 3 how are you?
But we can do a similar thing without the array and braces, using a variable argument list.
line_print( "hello", "janet", "how are you?" )
We rewrite our function like this:
function line_print(...)
for k,v in {...} do
print(k, v)
end
end
What is this ...
thing? it represents all the arguments to the function, not wrapped in a list, just open. Remember that a function could return multiple results, and we could receive them separately, like this:
a, b, c = get_three_results()
function get_three_results()
return 42, "hi there", 3.14159
end
And sure enough we’d have 42 in a
, “hi there” in b
, and pi in c
.
Now look back at my new line_print
function. Notice that I wrapped the ...
in braces, to get {...}
. That turns the separate arguments, however many there are, into an array. Frankly, in my opinion, if we’re going to process a variable number of arguments, it’s almost always perfectly fine to just stuff them into a list and use them that way.
There is a longhand way to do that, if you care to, like this:
function line_print(...)
local result = table.pack(...)
print(`There are {result.n} entries.`)
for k,v in result do
print(k, v)
end
end
That prints this:
[06:32] JR20250502 Variadic: There are 3 entries.
[06:32] JR20250502 Variadic: 1 hello
[06:32] JR20250502 Variadic: 2 janet
[06:32] JR20250502 Variadic: 3 how are you?
[06:34] JR20250502 Variadic: n 3
Some things of note with table.pack
:
It is variadic. You can provide any number of arguments and it will put them into an array and return it.
It also adds an element n
. the n
will be set to the number of arguments, the number in the resulting array part of the returned table.
The returned result has an array part, but it also has the n
. If we just want to iterate the array part, without the n
, we would use the ipairs
function, like this:
function line_print(...)
local result = table.pack(...)
print(`There are {result.n} entries.`)
for i,v in ipairs(result) do
print(i, v)
end
end
Now our program just prints the array part, not the extra n
.
Now, frankly, I think that we can do pretty much everything we need to do with what we know right now, but I would be remiss, I guess, if I didn’t at least mention select
.
Here are some tests for select
:
local arg = select(2, 10, 20, 30, 40)
assert_equals(arg, 20)
arg = select(4, 10, 20, 30, 40)
assert_equals(arg, 40)
arg = select(5, 10, 20, 30, 40)
assert_equals(arg, nil)
These tests pass. select
returns the argument that corresponds to its first argument, from the rest, starting at 1.
I tell a lie. What select really returns is all the arguments, starting from the one that corresponds to its first argument. In our test above, we just collect the first result. We’ll write a test showing all the results further down in the article.
So we can use select
with ...
— if we want to. First let’s do it, then let’s discuss why we might never do it again.
local arg = nth(2, 10, 20, 30, 40)
assert_equals(arg, 20)
arg = nth(4, 10, 20, 30, 40)
assert_equals(arg, 40)
arg = nth(5, 10, 20, 30, 40)
assert_equals(arg, nil)
We decide we want a function nth to do what select
does and we write it like this:
function nth(n, ...)
return select(n, ...)
end
And the tests pass. But here’s another version of nth
that also passes.
function nth(n, ...)
args = {...}
return args[n]
end
Remember that select
returns all the remaining arguments from its input, starting with the one whose index is the first parameter to select
Look at this test:
local twenty, thirty, forty = select(2, 10, 20, 30, 40)
assert_equals(twenty, 20)
assert_equals(thirty, 30)
assert_equals(forty, 40)
Yes. What select
really returns is all the arguments it’s given, starting from the starting value, down to the end, as separate items. Why might we want to do that? Well, I never have wanted to, in Lua, but perhaps it would be a decent way to treat a strided list of some kind.
It is possible that select
is faster than converting the ...
to an array with {...}
or table.pack
, but I would suggest that in most cases it won’t matter, and we really don’t know what rigmarole Lua does to deal with multiple returns. This article looks interesting, but I haven’t read it yet.
Let’s sum up, including as close to recommendations as I’ll come.
It is possible in Lua to write a function that accepts a variable number of arguments. We can accept any number of fixed arguments and then one batch of variadic ones. The variable part of the argument list is represented by ...
.
We can convert the ...
to an array with {...}
, or to a table containing the arguments as an array and also the number n
, the count of how many there are, with table.unpack(...)
.
We can access the k-th argument with select(k, ...)
, or with {...}[k]
. Or, of course, with select(k, ...)[1]
. Personally, I like the simple subscript form and I would save the arguments as an array, once and for all, at the beginning of the function:
function whatever(a, b, ...)
args = {...}
-- whatever we do here
end
Using {…} more than once would surely be inefficient, since the arguments would be stuffed into an array multiple times. Once should suffice.
As with any table, we can not only subscript it with integers or other keys, we can iterate it simply with:
for i, v in {...} do
end
It seems that the default iterator for a table is the pairs
one, but that’s just fine for our purposes. You could make a case for always using ipairs
or pairs
as you intend, to avoid confusion.
In my own work, I have rarely, if ever, really wanted a variadic function. More commonly I would provide a function that processes an actual table or array. The code for those is generally simpler, and I much prefer simple things.
You get to do things your way, of course. And if you have questions or comments, do drop me a line or an IM.
Safe paths!