Enhanced Variadic Parameters

Sorry I edited a few things in, typo fixes and more comments/examples.

I think maybe I wrote this too fast. If the disambiguation can also work for this case then it potentially means that there won't be any infinite recursion yes.

If someone with compiler insights can clarify this, I'd be happy to accept these rules for even more convenience, but if such disambiguation is not possible I would prefer to drop rules (2) and (3).

Does this get us on the same page now? :smiley:

1 Like

Next up ExpressibleByDictionaryLiteral. I personally don't like dynamic string part of this or even of @dynamicMemberLookup so I'm neutral on this part. However I have one specific question about this.

If you have an init such as this:

struct Baz {
  init(_: variadic [String, Int]) {}
}

How can a tooling then tell me which init I'm using, or jump to the declaration of it, if it looks like this?!

let baz = Baz(
  a: 1,
  b: 2,
  c: 42
)

Similarly,

struct Vector {
  var storage: [Int]
  init(_ x: Int, _ y: Int) {
    storage = [x, y]
  }
  init(_ coordinates: Int...) {
    storage = coordinates
  }
}

Vector(1, 2) // ?

As you might expect, or not, it prefers the more specific match over the variadic.

Yeah but in the above case it looks like I could in Xcode cmd + click on a or any other dynamic label. Can you imagine the tooling could work like this?

I don't see why it couldn't in theory, because the compiler could find the right initialiser, but Xcode behaviour isn't really in scope here. Xcode doesn't seem to be able to jump to the definition of an initialiser without labels as it is (except if you specifically write the .init), unless I'm missing a trick.

2 Likes

Yes I have and the workaround sucks.

This is a great suggestion!

Iā€™m also unsure about this. It may be that you have required keys or that you have a required number of entries regardless of the keys.

I left this off for the obvious reasons, but I think it should be considered eventually.

I don'y understand your concern here. There is nothing in this pitch that would break your example. It would still be supported. If we decided to deprecate the ... syntax you would just need to rephrase it like this:

  static func something(_ values: variadic [Int]) -> E {
    return .something(Set(values))
  }

The only semantic change in doing this is that your function would now accept arrays as well as variadic argument lists.

This is a good argument against deprecating the ... syntax, something this proposal does not do.

This is correct. But I am not proposing that we deprecate or change the ... syntax.

I think accepting arguments of the collection type generally makes sense - I have written way too many forwarding overloads and grumble every time I have to do it. However, it may make sense to consider a modifier that can disable this behavior for specific parameters in order to support extensions that forward to non-variadic methods provided by a third party library. How often do you want to do this?

I'm not a tooling expert but I don't see any reason why this would be any more difficult than it is for the existing variadics. The compiler obviously has to know what is called and will report that to the tooling.

1 Like

As far as my personal observation goes this issues you are talking about only exist if you start variadic.

  • Is you start non-variadic and then need variadic convenience then it's always "okay now I need to write an extra overload and simply forward the call".

  • If you do it the other way around then it's always "ah crap now I need a non-variadic overload and I have to re-write and move everything into non-variadic overload".

I understand the frustration there, I've been there myself, and it would be great if we could solve this. An example with an enum case that has variadic payload would demonstrate the frustration of starting the wrong way more clearly.

However I mentioned above that to support all 3 rules you're proposing you will need an extra rule for disambiguation, if that's not possible both rules (2) and (3) create more harm than good in my opinion. variadic would allow more types, but this new construct won't have the same benefits with retroactive extensions as ... has. So my conclusion is to require such a disambiguation rule or dropping both rule (2) and (3).

I don't follow what rules go with the (2) and (3) labels in your post.

If the core team feels a disambiguation rule would work here that makes sense to me. It seems fine to say that a non-variadic overload is preferable to a variadic overload when a collection is provided. If this doesn't work another option is (using strawman syntax) @literalOnly variadic which would explicitly disallow the use collection values. Either way, I don't think this is a deal breaker - there are ways to support your use case.

Might have overlooked this in the pitch, but how you distinguish with the below example between adding int values or adding up arrays?

func add(values: variadic [T]) -> T {...}

let sum = add(values: 1, 3, 5)  /** sum = 9 */
let sum = add(values: [1, 3, 5], [8, 9])  /** sum = [1, 3, 5, 8, 9] */

 /** is this adding ints to 9 or adding arrays to [1, 3, 5] ? */
let sum = add(values: [1, 3, 5])

I don't have an answer to this but I personally had recently an idea of something like this that would (partly) cover the pitched rule I refer as (1) in this post.

func foo(_: T... as Set<T>)

@anandabits you can use this as alternative solution if you want. ;)

The design is to have the compiler rewrite add(values: 1, 3, 5) as add(values: [1, 3, 5]). That means add(values: [1, 3, 5], [8, 9]) is invalid code. If you want your function take a variadic array of arrays your declaration would need to be add(values: variadic [[T]]) -> T . In this case the first example would be invalid.

I'll be sure this gets in "alternatives considered" if an implementation comes along and we can move this to review.

Hmm, so if the compiler rewrites add(values: 1, 3, 5) to add(values: [1, 3, 5]), why doesn't it rewrite add(values: [1, 3, 5], [8, 9]) to add(values: [[1, 3, 5], [8, 9]]), where each T is of type array?

1 Like

(Assuming the declaration was meant to be func add<T>(values: variadic [T]) -> T {...})

Are you proposingā€¦

  • Disallowing T to expand to Array<_> because the function is variadic?
  • Disallowing T to be inferred from an array literal because the function is variadic?
  • Something else?
1 Like

A very simple mental model of the proposal is

variadic T: ExpressibleByArrayLiteral

When you use your API in a variadic way the compiler will extract Element type as T.ArrayLiteralElement.

Therefore foo(_: variadic [[Int]]) is the correct way if you want to write foo([1, 2], [1, 2]).

@anandabits in fact I think we could upgrade T... to the same functionality and soft-deprecate it like class and AnyObject. What do you think?

T... /* implicitly as Array<T> */ == variadic Array<T>
T... as Array<T> == variadic Array<T>
T... as Set<T> == variadic Set<T>
T... as R == variadic R where R: ExpressibleByArrayLiteral, R.ArrayLiteralElement == T

I think the answer to this would come up by itself if you try to implement add :) you either constrain T to a type that has a sum defined (so you cannot pass multiple arrays), or a collection (so you cannot pass direct values)

A potential ambiguity that was brought up in the previous discussion of variadic splatting involves variadic Any:

func foo(_ x: Any...) {
  print(x.count)
}

let a: [Any] = [0, 1, 2]
let b: Any = a as Any

foo(a)          // 1 arg or 3?
foo(b)          // 1 arg or 3?
foo([0, 1, 2])  // 1 arg or 3?

Currently, all 3 calls are legal, and they all pass in 1 argument. If we want to allow passing a collection (whether literal or variable) into a variadic, we need an answer for this situation.