Explicit array splat for variadic functions

I've been getting annoyed recently writing two copies of several initializers for a library, where composed objects can either take a variadic list of values or an array. E.g.

struct Foo<T> {
    // ...
    init(values: [T]) { // ... }
    init(values: T...) { self.init(values: values) }
}

struct Bar<T> {
    // ...
    init(foos: [Foo<T>]) { // ... }
    init(foos: Foo<T>...) { self.init(foos: foos) }
}

Basically, if you'd like to give consumers of an API the option to use the variadic initializer or an array, you need to write a bunch of unnecessary initializers. Go addresses this problem with an explicit "splat" operator:

package main

func my_func(args ...int) int {
   sum := 0
   for _,v := range args {
      sum = sum + v
   }

   return sum;
}

func main() {
    arr := []int{2,4}
    sum := my_func(arr...) // sum = 6
    sum2 := my_func(2,4,6) // sum2 = 12
}

I think this would be a nice addition to Swift and would make variadics much less of a pain to work with.

9 Likes

What’s your use case for this? Why not offer just the array?

Imho there has been a much better suggestion in the past:
Ditch variadics as they exist now completely, and add an option for parameters that are ExpressibleByArrayLiteral to accept such a literal as well as an existing value of that type.

func sum(array: [Int]) -> Int {
    return array.reduce(0) { $0 + $1 }
}
print(sum(array: 1, 2, 3))
print(sum(array: functionThatReturnsArrayOfInt()))

Instead of adding complexity to the language, this would make Swift simpler:
"T..." is a strange type that can't be used in normal declarations and magically turns into an array when it is allowed.
That reduction of complexity alone imho would be enough justification - but there's more:
Array is not the only thing that is ExpressibleByArrayLiteral. So if you consider variadics to be a good feature, why not allow them for Sets and other types?

4 Likes

The use case is API design.

Some domains are better serviced with a variadic method: databaseTable.select(a, b, c). So you define a variadic method.

But you also have to add a method that accepts an array, so that your API can be used dynamically: databaseTable.select(columns).

You end up with two methods, and a lot of boilerplate. This is the problem that needs addressing, the topic of this thread.

The suggested solution is to only define the variadic method databaseTable.select(a, b, c), and get automatic support for arrays with the databaseTable.select(columns...) syntax, inspired by Go. Ruby uses the splat operator databaseTable.select(*columns), and this is interesting as well.

10 Likes

I see three courses of action here:

  • @Tino's option to make a, b, c, d, ... a separate literal and make ExpressibleByArrayLiteral types conform. However, this has issues like the ability to actually use that new literal to define arrays.

  • Make functions with T... parameters accept ExpressibleByArrayLiteral types by adding some compiler magic.

  • " Take T... out " of the magical world and make it an actual type with it's own a, b, c, ... literal, additionally making ExpressibleByArrayLiteral types implicitly convertible to that new type. Pretty sure that can be done with existing protocols.

IMO, the second and third options are the most sensible, not being sure about the trade-off balance between those two.

2 Likes

I guess I didn't explain correctly... the idea is not to add any new literal, but simply allowing to leave out the "[" and "]" at call site.
Then, there would only be functions that accept arrays (and other types), and you could decide freely wether to pass a normal array or save two characters, as you can do with todays variadics.

@Tino: it looks like it would then become impossible to have two function overloads, f(T) and f([T]). Wouldn't f(1) become ambiguous?

1 Like

Like with todays variadics, there would be overlap in such a case, and the original idea used an attribute to make sure there can't be any ambiguity ([Proposal] Variadics as Attribute).
But imho that's not such a big problem in real world situations, and with or without an attribute, I'd consider this to be a significant improvement.

Like with todays variadics, there would be overlap in such a case

In today's variadics, yes. But not in your tomorrow: after variadics are removed, the field is open for overloads.

But imho that’s not such a big problem in real world situations, and with or without an attribute, I’d consider this to be a significant improvement.

As you say, that's an opinion :-)

And thanks for the link, it's an interesting read.

Just to recapitulate the current situation:
Someone designs an algorithm that works on an array, than use some magic in the function declaration (those ...) so that you can pass something that doesn't look like an array, but rather like a bunch of unnamed parameters.
With splatting, we would add just another layer on top of this...
So, for a simple function that works on array, we would have the special "..." syntax at declaration side, and a special splatting-synax at call site. For me, that's completely nuts, even if the compiler doesn't actually do all the transformations (Array -> Variadic -> Array).

My expectation for a function which can either take a single argument or an array is that the latter just puts the single argument into an array and calls the first - I can't think of any good examples to do it differently.
@Haravikk3 idea would be a fundamental change on what variadics are, but required changes when using them would be trivial:
Everything would stay as it is, you just would replace all those magic "T..." with "@variadic [T]".

I agree that something needs to be done about this, because I quite frequently find myself weighing the pros/cons between allowing variadics and also writing the boilerplate to allow both variadics for developer friendliness and arrays for more dynamic usability. I generally find myself doing something like this:

class HelloWorld {
    var names: [String]
 
    // This becomes ambiguous with the variadic, so just remove it
    // init(_ name: String) {
    //     names = [name]
    // }

    init(names: [String]) {
        self.names = names
    }

    convenience init(_ names: String...) {
        self.init(names: names)
    }

    func sayHello() {
        names.forEach { print("Hello \($)") }
    }
}

This allows me to write:

let hello = HelloWorld("Jacob", "Tim", "Steve")
hello.sayHello()

As well as use this dynamically:

class Goodbye {
    var hello: HelloWorld

    init(_ names: String...) {
        // Cannot initialize using the variadic since names is converted to an array
        hello = HelloWorld(names: names)
    }

    func sayGoodbye() {
        hello.names.forEach { print("Goodbye \($0)") }
    }
}

This is a poor example since in this case you would just subclass HelloWorld and add the sayGoodbye() function, however, it still demonstrates the principle that causes a lot of developers headache. We need splatting (or something else) to unify variadics and Collection or Sequence types. This would eliminate boilerplate code many of us are forced to use right now if we want both the variadic simplicity and array dynamic usability.

I also don't like how we are limited to only 1 variadic argument per function. Solving the issue in this pitch would remove that restriction.

I'm in favor of auto-splatting variadics into their corresponding Collection/Sequence type (so that this could work with both Arrays and Sets), but that's because I've only every seen that (since I've programmed in Ruby). I am definitely open to other suggestions that may possibly be better.

It would be nice to keep the ... syntax rather than needing to use an @variadic attribute at the function parameter site.

If we could turn variadics into something like this:

class HelloWorld {
    var names: Set<String>
    init(_ names: Set<String>...) {
    }
}

And then the compiler knows that names is either a Set<String> or a variadic to be converted to a Set<String> that would be great. If you wanted an Array<String> then you'd say [String]... instead.

This would be very useful. Sometimes you have a variadic parameter that you want to forward to another variadic function.

For example, say you wanted to define a conditional print function that only prints based on some condition:

func myPrint(_ args: Any...) { 
  if condition {
    print(args...) // hypothetical syntax
  }
}

Currently, there's no way to write this without gross hacks.

9 Likes

I think most people are in favor of this (though clearly not all, as this thread shows!). However, the syntax is at odds with the one-sided range syntax added in Swift 4, and so someone has to figure out how to either disambiguate or what syntax would be good to use instead. (The other languages I know of use either array... or *array for this, or just array with no adornment at all.)

The next step, of course, is implementation. Unfortunately I don't know enough about the expression type checker to say how tricky that is.

Is this really a collision with the one-sided range operator? The one-sided range operator is always used in a subscript isn't it?

I don't think there should be any ambiguity if one-sided ranges are always within [] and this would be outside of [] and at a function declaration.

That being said, if there does end up being a collision with the one-sided range operator, I think it would be acceptable to use a splat operator like * at the front of an array variable.

My other question though is, once we get splatting of some sort, what will we do about array vs variadic functions/initializers? Do we allow both to be declared or will one overwrite or collide with the other? Will there be instances where the developer does not want to allow a function to be used variadically? If not, then will there be the possibility of making compiler magic just say variadics always map to Collection/Sequence parameters?

No, it's perfectly legal to use one-sided ranges as an infinite sequence:

zip(42..., ["a", "b", "c"]).forEach { print($0) }

A variadic parameter is declared with syntax that appears to claim it has type “T...”, for some T. It seems natural that if you have a parameter masquerading as type “T...”, you should be able use it as a value of type “T...”.

In other words, no “splatting” operator is necessary, and if such an operator existed then it would be actively detrimental to the understanding of the code.

Adapting Lance’s example:

func myPrint(_ args: Any...) { 
  if condition {
    print(args)
  }
}

Here we take a variable of type “Any...” and pass it to a function which accepts…an argument of type “Any...”. This should just work.

Notably, this functionality is essentially unrelated to the “tuple splatting” that was removed in SE–0029. In that case, converting between a tuple and a function’s argument list, the type is changing so it makes sense to have an explicit operator to perform the conversion. But here no such modification occurs.

4 Likes

So the desired approach is some more compiler magic? If that is the case, it would really be best not to involve any new or existing operators in this.

To everyone
Is there someone who thinks an "operator" is really necessary? If so... why?

2 Likes

I don't remember on what list, or precisely when, but shortly before the switch to the forums, I remember having seen a mail-thread about possibly having variadics being, at the ABI level, a compiler-generated buffer type instead of an actual Array, which if extended to a source level type would provide this behavior.