6 Jun, 2024

Swift Variadics and where (not) to find them

Variadic functions are a clever way of implementing methods that take as input a variable number of arguments, using a curious ellipsis () syntax.

They are present in many programming languages, coming to Swift all the way from C, and Apple has being explicit that they definitely have a place in the language. We can find tons of Foundation APIs leveraging them:

func print(
    _ items: Any...,
    separator: String = " ",
    terminator: String = "\n"
)

print() 
print(1)
print(1, 2, 3) // All valid calls for the variadic print() function

Cool! So what's the deal?

Although being a first-class citizen, variadics should be used with caution as they can lead to poor design choices, losing most of their syntactic sugar appeal.

Let’s see this with a classic example. Suppose we want to reinvent the wheel and implement a sum function. We might go with this:

func sum(_ numbers: Int...) -> Int { 
    numbers.reduce(0, +) 
}

Clever! We can now call sum() directly with an arbitrary number of Integers. sum(), sum(1,2,3), sum(1,2,3,4,5,6,7) are all valid.

As we can see, at the implementation level, variadics end up just being arrays of their underlining type (in this case, an array of Int). So you might expect that you can also use an array on the call-site, meaning that sum([1,2,3]) should be equivalent to sum(1,2,3), right?

Right?.
func sum(_ numbers: Int...) -> Int {
    numbers.reduce(0, +)
}

let numsToSum = [1,2,3]
sum(numsToSum) // ❌ “Cannot pass array of type '[Int]' as variadic arguments of type 'Int’.“

The answer is no! There is actually no way of using an array when calling our sum() method 🤯

A way to workaround with this would be to overload the method with one that explicitly takes an array of Int:

func sum(_ numbers: [Int]) -> Int {
    /// main logic of our function
    numbers.reduce(0, +)
}

func sum(_ numbers: Int...) -> Int {
    /// wrapper of the non-variadic variant
    sum(numbers)
}

let numsToSum = [1,2,3]
sum(numsToSum) 
sum(1, 2, 3) // now both return 6!

This way, the variadic function ends up being a convenience wrapper for the non-variadic function.

But it makes you wonder why not avoid variadics in the first place, as they could end up cluttering the code for this marginal syntactic sugar. Even worse, if you are just consuming the API, it could end up being unusable altogether, as you do not have ownership to overload the method.

There have been many (1, 2, 3, 4) evolution proposals to either add the splat operator that many other languages have, which would allow unpacking arrays and using them as variadic input, or even just being able to accept array inputs, but none are appareantly on track for getting implemented. Personally, I have to think twice each time I consider them, which sucks because they do look cool!

I hope you found this article helpful. For any questions, comments, or feedback, don't hesitate to reach out: connect with me any of the social links below, or drop me an e-mail at marcos@varanios.com.

Thank you for reading! 🥰