Variadic Parameters that Accept Array Inputs

foundation

#6

If we ever get fixed-length arrays, we could certainly convert variadics to that type. I think the benefits associated with knowing the count are orthogonal to whether the compiler allows some kind of automatic coercion between arrays and variadic parameter lists.


(Wildchild9) #7

This seems redundant, if a variadic function is used as array in a function, then it should also be able to take an array as an input. You can always overload the function, but it would be much more ideal if the variadic parameter itself was just more flexible.


(Wildchild9) #8

What if Variadics where their own Collection type?

If we had this, we could use Type... and also Variadic<Type> as the same input. This would solve many problems that have to do with the passing and use of variadic parameters.


#9

It is redundant, but ofttimes, the actual implementation of a feature requires it. To be able to pass either a parameter list or an array to the same function requires special handling. The question is, how are the parameters handled today, and what are the minimal set of changes required for the compiler to allow an array to be passed instead?


#10

The collection would have to be ordered, since the compiler could never make the assumption that the function doesn't care. So you've really just reinvented Array. So just make Type... shorthand for [Type(), Type(), Type(), ...] and be done with it.


(Itai Ferber) #11

How does this resolve the ambiguity of

func doAThing(with items: Any...) -> [Any] {
    return items
}

let res = doAThing(with: 1, 2, 3)
let res2 = doAThing(with: res)

Does the second call to doAThing receive three arguments (1, 2, 3, splatting res), or only one ([1, 2, 3], since res can be promoted to Any)? Or is this now a compilation error (which is potentially source-breaking)?

I've been bitten both ways in other languages which support argument splatting in this way; it'd be nice to offer a less ambiguous and surprising solution.


(Wildchild9) #12

@itaiferber, That is why I think that maybe we can make a flexible Collection type called Variadic, or even a Variadic type cast for Array. This would prevent this issue, what do you think?


(Nobody1707) #13

Now that SE-0213 has passed, you could solve this by making varargs their own logical type that implicitly converts to (but not from) [Element] with initializers that take an [Element]. The type itself will probably have to remain compiler magic for a while, but that shouldn't be too big of an issue.

// Probably predefined in the standard library. _VarArgs is not meant for direct user access
// because it's wicked black voodoo magic. The function name is just a strawman.
@inlinable
public func splat<T>(_ array: [T]) -> T... {
    return _VarArgs(array) // Converting init.
}

func doAThing(with items: Any...) -> [Any] {
    return items // items implicitly converts from Any... to [Any]
}

let res = doAThing(with: 1, 2, 3)
let res2 = doAThing(with: res) // returns an [Any] whose sole element is an [Any]
let res3 = doAThing(with: splat(res)) // returns the same thing as doAThing(1, 2, 3)

(Itai Ferber) #14

I'm not sure that would resolve the issue here on its own. Array itself here isn't really relevant — the issue is that Variadic<Any> is implicitly convertible to Any:

func doAThing(with values: Any...) -> Variadic<Any> {
    return values
}

let variadic: Variadic<Any> = doAThing(with: 1, 2, 3)
let huh = doAThing(with: variadic /* is this converted to Any, or splatted? */)

We could decide that the semantics of variadics are special — passing in a Variadic<Any> (however this might be spelled in reality) into a function taking Any... could more strongly bind the contents of the variadic rather than passing the collection itself.

My question then becomes, what happens when you do this:

let variadic1 = doAThing(with: 1, 2, 3)
let variadic2 = doAThing(with: "foo", "bar", "baz")
let variadic3 = doAThing(with: variadic1, variadic2)

Are variadic1 and variadic2 splatted in there, or are they passed in verbatim?

  • On the one hand, we could decide that if the type matches the argument type exactly (Variadic<Any> == Variadic<Any>), we splat the contents, such that doAThing(with: variadic1) passes in the contents of the variadic, while doAThing(with: variadic1, variadic2) pass the collections in as-is

  • On the other hand, we could decide that we never splat automatically and always prefer the upcasting rules, and if you want to splat, @Nobody1707's suggestion is an explicit way to do it (i.e. you'd have to write doAThing(splat(variadic1)) to pass in its contents). We could even special case the syntax with something like *vars so you can express all of the following:

    1. doAThing(with: variadic1, variadic2) /* no splatting */
    2. doAThing(with: *variadic1, variadic2) /* splat the contents of variadic1, pass in variadic2 verbatim */
    3. doAThing(with: variadic1, *variadic2) /* and vice versa */
    4. doAThing(with: *variadic1, *variadic2) /* splat everything */

There's a sort of self-consistency to both approaches:

  • The first prioritizes type-level consistency: given an exact type match, no upcasting should be necessary [at the cost of making doAThing(with: variadic1) do something inconsistent with doAThing(with: variadic1, variadic2)]
  • The second prioritizes call sites doing the same consistent thing, ignoring type matching [doAThing(with: variadic1) behaves the same as doAThing(with: variadic1, variadic2)]

Because we don't have a way to fully express this today, the current behavior matches #1 (with Arrays being imperfect type matches):

func doAThing(with values: Any...) -> [Any] {
    print(values.count)
    return values
}

let a1 = doAThing(with: 1, 2, 3) // prints 3
let a2 = doAThing(with: a1) // prints 1

But there is no current precedent for what would happen here should we be able to return Any...


For what its worth, I think we can come up with a good solution either way — these are just edge cases that immediately come to mind that I think a pitch should address. :slight_smile:


(Nobody1707) #15

I guess the real question here is: which is more obvious to the people reading the code. Manual splatting:

let a1 = doAThing(with: 1, 2, 3) // prints 3
let a2 = doAThing(with: a1) // prints 1
let a3 = doAThing(with: splat(a1)) // prints 3

Or manual casting to Any:

let a1 = doAThing(with: 1, 2, 3) // prints 3
let a2 = doAThing(with: a1 as Any) // prints 1
let a3 = doAThing(with: a1) // prints 3

My gut instinct is that the former is more obvious than the latter, but there's an argument both ways.


(Howard Lovatt) #16

Why not scrape variadic arguments, it’s not hard to type [].


(Wildchild9) #17

I think that in let variadic3 = doAThing(with: variadic1, variadic2), variadic1 and variadic2 should be considered as the own collections, Array. But yes, I do think it is a good idea to have a splat¹ – as an extension and even a function – available for Variadics.


¹. We could honestly get this to be available for any Collection, this so we can splat each of element of any collection down to its individual elements as now type Variadic<T>.


(Braden Scothern) #18

My thought is that Any... should be sugar for Variadic<Any> and that the behavior should be similar to this:

func doAThing(with values: Any...) -> [Any] {
    print(values.count)
    return [Any](values)
}

let a1 = doAThing(with: 1, 2, 3) // prints 3
let a2 = doAThing(with: a1) // prints 1 (you have an array not an A...)

func doAnotherThing(with values: Any...) -> Any... {
    print(values.count)
    return values
}

let a3 = doAnotherThing(with: 1, 2, 3) // prints 3
let a4 = doAnotherThing(with: a3) // prints 3
let a5 = doAnotherThing(with: a4, a4) // prints 2

If Variadic<T> instances can only be created as you go into a function there should also be an explicit .splat so you can do this:

var a6 = doAnotherThing(with: a4.splat, 4) //prints 4

That said, being able to say something along the lines of a6.append(4) might be useful?


(Nobody1707) #19

Variadic arguments were never about being easier to type, they've always been about being easier to read.


(Félix Fischer) #20

This is what I was gonna say as well.

Variadic arguments and arrays aren't the same. But variadic arguments and fixed-length arrays are the same. If/when they are implemented, we could work with variadics interchageably as that type.


(Wildchild9) #21

But instead of casting it like [Any](values), it should probably be Array(values).


(Braden Scothern) #22

Very true! I am not a fan of that casting syntax so I avoid it most of the time. I don't know what I was thinking.


(Wildchild9) #23

One last thing I think I forgot to mention, when actually using an input of Variadic<T>, you should be able to use it with all/most if the same things you can do with an Array for the most part, this would avoid excessive type conversions and make them much easier to work with (more versatile). Like you should have to type cast – Array(someVariadic) – a variable holding Variadic<T> when putting it in to a parameter of type Array<T>, but from some cases like this, they should for the most part be able to be passed around and what not like Arrays. You should also be able to do Variadic(someArray) (initialize a variadic collection with an Array, or any other Collection for that matter). But this would not negate with still having the option to do either splat(someArray) or someArray.splat.

Thoughts?


(Wildchild9) #24

Variadic arguments are also a much nicer syntax for inputting multiple parameters.


(Howard Lovatt) #25

Same comment re reading, the brackets are small enough to ‘disappear’ when reading.