Enhanced Variadic Parameters

I don't know what “does not play well with retroactive extensions” means exactly. Retroactive extensions can already cause issues with clashing names/signatures. Are any of your concerns here specific to this proposal?

extension Array {
  subscript(_ i: Int) -> Int { return 99 }
}

let a = [0, 1, 2, 3]
a[0] // error: ambiguous use of 'subscript(_:)'

In one sentence: The two extra rules prevent you making non-variadic API you don't own variadic.

Oh, with the same base name, sure, without some “specificity rule for disambiguation” like I mentioned. I don't see how this holds though, then

This is a possible disambiguation rule, and I don't see how this “completely defeats the purpose” because it allows you to write a single function instead of two in the common case.

I said it multiple times already, this is only true for code you own. I think the general rule should be that you should design your API non-variadic first, if you users then want to make some parts of it variadic that should be able to without any need of creating alternative names. That is already possible with [T] and T... today. This should remain possible with R: ExpressibleByArrayLiteral and variadic R. If on the other hand you decide to go variadic form the beginning, fine there won't be any issues for your users as they still can choose how to use your API, either in variadic or the non-variadic way, but that requires you to provide a little overload, the same way your users could transform your non-variadic API into variadic API.

If on the other hand variadic would have the two extra rules, you will lose that ability.

I think the ability of making non-variadic API retroactively variadic is a fair trade-off and worth a little custom overload.

Code you own is the common case, yes. In what way would a disambiguation rule like the one you mentioned hurt either “code you own” or “retroactively making someone else's code variadic without creating alternative names”?

Let's say R is a type that conforms to ExpressibleByArrayLiteral, for simplicity we ignore any Element type.

As currently proposed a single variadic construct should cover three cases:

  1. You should be able to omit []
  2. You should be able to pass an array literal to variadic R
  3. You should be able to pass an instance of R to variadic R

In current Swift T... allows only (1) and it generates always an instance of [T].

A library author can decide if the library should require only R or can make it variadic R. In case of variadic R there won't be any issues at all, not for the library itself nor for its users. However if the author goes with non-variadic approach then the library user may want to overload existing non-variadic API with a variadic version.

Rules (2) and (3) will generate an ambiguity if the overloaded API share the same signature except for the now variadic type. I mentioned above that we could say that the compiler can in this case prefer the non-variadic API. That however makes these two rules less useful. Furthermore the variadic overload can potentially be impossible because it will cause an infinite recursion (like showed above), but that is again only caused by rules (2) and (3).

So far these issues arise for functions/methods, but what about an enum payload?
If the enum case for enum from a module you don't own is non-variadic, the user won't be able to extend it to make the payload seem variadic, it will require awkward new names.

extension Optional where Wrapped: ExpressibleByArrayLiteral {
  static func some(_ wrapped: variadic Wrapped) -> Optional {
    // with presence of rules (2) and (3) this is an infinite recursion
    return .some(wrapped) 
  }
}

// with only rule (1) the use can now write
Set<Int>?.some(1, 2, 3, 4, 5) 
// if rules (2) and (3) are there the following two examples ambiguous 
Set<Int>?.some([1, 2, 3, 4, 5]) 
Set<Int>?.some(intSet)

That said, allowing rule (1) is a huge enhancement, but I do not support rules (2) and (3) because of the disadvantages they come with.

I think you've made a great argument for a disambiguation rule to better support retroactive conformance, but I'm still not convinced that such a rule is impossible. I only just saw the recursion example above (did you edit it in?), but:

With your proposed disambiguation rule, essentially preferring the non-variadic overload when otherwise ambiguous (a nice match to other existing “more specific” disambiguation rules), wouldn't the “infinite recursion” line actually call the version from module A, it being the more specific match for Set<Int>, and therefore not be an infinite recursion? What am I missing here? And how does this make the rules “less useful”? Similarly for your example in this post (why wouldn't .some(wrapped) prefer the non-variadic version, and it would leave neither of the last two examples ambiguous).

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. ;)