When to use varargs
Programming languages often support some kind of variadic functions — functions that can take an unbounded number of arguments – that are often referred to as varargs functions. Many of these languages also support some form of collection literals. When is it better to take varargs over a list of args?
First up some negatives: varargs constrain future changes to a functions argument list. Languages with optional or defaulted arguments in many cases allow for extension to a functions arguments without breaking callers, but if the function accepts varargs, then this may be precluded. There is also potential for collisions with other function overloads.
Additionally, while varargs may improve the usability of a function with static varargs, it decreases the usability when it has dynamic varargs. How much this shifts in either direction depends on the syntax provided by the language in question.
Rather than blathering on in abstract, let's look at some practical examples (from garbage collected languages) of varargs.
The JavaScript Array
type manages to cover off a number of interesting cases. Comparing it to the Map
and Set
types introduced a decades and change later is also interesting. I’ve used TypeScript type notation for clarity.
new Array<T>(length: number)
and new Array<T>(...items: T[])
are the original overloads for the Array
type’s constructor1. This is a good example of the clash of overloads and varargs. The specific clash occurs when T
is number
: You cannot construct a list of a single number without a special case. This is a great space for potential bugs if the function constructing this array takes a T
of its own: it then also needs to check the type of its arguments. This is one of a number of ways in which the Array
type tries to be too clever in what it accepts, and ends up making an annoying edge case.
More recent JavaScript includes Array.of<T>(...items: T[])
and Array.from(items: Iterable<T>)
. Here .of
takes a varargs of items, while .from
takes a iterable object. I like splitting both ways of constructing the collection out into their own static methods. There is never any confusion around overloading and there is no expectation that you will need to pass any other arguments, and it leaves the primary constructor free in this case to be about defining an empty array of a given length.
Curiously Map
and Set
don’t have these static methods. They both just take an iterable in the constructor, no varargs. My best guess is that .of
and .from
are intended to work around the messy old constructor overloads, so are not needed on new collections that just do the right thing out of the box.
Sometimes it is useful to create something like a function decorator – a function that takes a function as an argument, and returns a new function that wraps that up. Here’s a simple decorate that logs the arguments passed in as an example:
function logArguments(f, name) {
return function (...args) {
console.log(name, args);
return f(...args);
}
}
This is a cut and dried use-case for varargs. Without it you would need to write a version of logArguments
for every number of arguments that f
could take.
String formatting functions are a pretty interesting case. The first case of varargs I ever encountered was printf
in C. Just about every language has a format function of some kind these days, and a lot of them can probably be traced back to the printf
family.
Python’s string formatting functions are interest here as the old approach took a collection as an argument to the %
operation, while the new approach has a method .format
that takes varargs. Very quickly, here is a basic comparison:
"foo %s %s" % ("baz", "bop")
"foo {} {}".format("baz", "bop")
This use of varargs optimizes for the very common case with format strings where the arguments are statically defined. Even if there is some kind of internationalization occurring on the format string itself, the arguments will be fixed. By chosing to use varargs, and especially Python’s keyword varargs support, the API here is a lot more obvious than the old operator style. Here’s the keyword version for comparison:
"foo %(quux)s %(bop)s" % {"bop": "baz", "quux": "bop"}
"foo {quux} {bop}".format(bop="baz", quux="bop")
Python’s keyword arguments are often leveraged by language users for tiny little DSLs. The Django ORM query API is a solid example: you can think of it as a clever use of keyword arguments combined with a builder (building up a query set). This relieves the problems of difficulty extending the API by simply allowing the maintainer to add new builder operations rather than having to extend the one function.
Here’s one where it seemed like the right choice at the time, but I later regretted it:
(defn call
"Does a syncronous http request to the server and method provided."
[endpoint-url method-name & args]
(let [call (methodcall/MethodCall. method-name args)
body (-> call methodcall/unparse emit with-out-str)]
(-> (http/post endpoint-url {:body body
:content-type "text/xml;charset=UTF-8"})
:body
to-xml
methodresponse/parse)))
Later, consumers of the library needed to provide additional parameters to the underlying http plumbing library (the call to clj-http’s http/post
function). To support this, and not break the API existing users I ended up creating call*
that call
delegated to.
Some observations from the above. Some places varargs can work well:
- Decorator functions. This is clear cut; doing it without varargs would be far worse.
- When the argument list is very context dependent and doesn’t change. String formatting for example.
- When all the arguments are varargs. Collection constructors for example.
- DSLs with builder style APIs.
- Languages with first class keyword args and varargs like Python.
Places to avoid them:
- Functions with overloads, especially when the varargs form is generic, such as the
Array
constructor. We want to avoid ambiguity around dispatch such as thenew Array(1)
case. - Code with external consumers. Both the JavaScript
Array
examples, and the Clojure example above, had to do sub-optimal things to support the future users properly. If you want to provide a varargs interface, delegate it to the collection taking version by default.
- Ignoring the fact that there is no value to using the varargs form of the constructor as the language has an equivalent literal form.