Another attempt at passing arrays as varargs (with implementation)

@xwu That's a really interesting idea, I think I'd support using that syntax so long as the coercion always needed to be written out explicitly. I'll have to think it over a bit more, but I don't think it would have any ambiguity issues either.

I'm definitely open to revising this pitch to replace #variadic with nicer syntax. I do feel pretty strongly that anything we choose should be both explicit and fully source compatible though, for the reasons outlined above (looks like your idea meets those requirements).

1 Like

I like this syntax much better than having to use #variadic, but would this be an issue considering people may then have an expectation for conditional or forced casts using as? and as! respectively?

Should those be disallowed entirely or would it be best to allow them, but they would always succeed as long as the type is an array and fail otherwise? Or could this be how non-array types can be used as variadics? Perhaps Collections would succeed under conditional/force casts but other unrelated types would fail?

Just throwing things out there to think about, but I would definitely prefer as [Type]... over #variadic.

Good point!

I think it might be best to only allow as, with a fixit for as? or as! if we use this syntax. Coercion from an Array to variadic will never fail, and I'm not sure it would be a good idea to combine, say, a failable cast from Any to [T] and a coercion from [T] to T... in a single operator.

I'm still opposed to integrating support for arbitrary Collections in this pitch as it adds too much complexity. I don't think this design would prevent such a feature from being proposed sometime in the future though.

+1 to the proposal with an extra +1 to @xwu's suggestion to use as T... for the splatting syntax.

2 Likes

Thank you for driving this forward!

My preference would be to go with @GetSwifty's suggestion, allowing an unadorned array to be passed in when there's no ambiguity, and emitting an error or warning when there is. It still counts as source compatible if we maintain existing behavior but emit a new warning right?

If that's not in the cards, the as T... syntax seems like the next best alternative to me.

1 Like

I don't like having to repeat the type. What about this:

foor(bar: [1, 2, 3] as ...)
1 Like

Variadics only support arrays and other Collection Types, so I think that makes sense until that support is (if ever) added.

If implicitly converting an Array to a _basically_an_array_ is too crazy :wink: I'd be more than happy with foo as T...

To me ... isn't a Type, while T... is. It also looks too similar to the Range syntax IMO.

1 Like

Swift lets me omit the type parameter in, for example, let x = 1 as Optional. Why shouldn't it let me omit the type parameter in f(array as ...).

1 Like

I think T... is better thought of as a sugared type. Swift doesn't allow as ?, as [], or as [:] today, so it shouldn't allow as .... It would also likely lead to very poor diagnostics in the case of typos/programmer error if we allowed omitting the base type.

6 Likes

I just want to remind everyone that the ambiguity exists exclusively when the variadic parameter’s type is an existential for a protocol to which Array conforms. In practice, this means there is ambiguity for Any..., for P... where P is a protocol that Array conforms to, and for T... where T is a generic placeholder (which could represent an existential).

Thus, when the variadic parameter is a concrete, non-existential type, we know that it is unambiguous.

8 Likes

Right, It seems like a pretty constrained corner case. It doesn't seem like it would effect that many people and it seems like it's an easy warning/error to deal with.

2 Likes

I'd actually expect this case to come up relatively often in practice when using this feature because print takes an Any... argument.

Personally, I think it would be a bad idea to introduce new implicit conversions in the type system to support this feature. Consider the fact that implicit bridging conversions were intentionally removed in Swift 3. One reason was because bridged types had no obvious subtyping relationship; The same is true of T... and [T].

Even if ambiguous cases are infrequent, when they do occur, they are likely to be very surprising to users who don't understand the intricacies of what would be a relatively obscure typing rule.

4 Likes

Except T... IS an Array...after it's compiled. There's even an argument that it's more correct to convert T... to Array<T>. #BikeShedding

1 Like

Maybe it's just me, but I don't really see this as an implicit conversion issue. T... isn't a regular type -- you can only use it as a function parameter, and once you start using the parameter you actually have a [T]. For that reason I see it as a special-case alternate spelling of [T] (in fact, if we were to redesign the feature from scratch I would want to spell the parameter's type as something like @variadic [T]). From that viewpoint, it feels very natural to be able to pass in an unadorned array into that parameter.

4 Likes

I think the better analogy is between T! and T?.

In þe olde days, T! was its own type. Then it became an alias for T? with certain compile-time behavior.

Currently, T... is distinct from [T], but in the grand future it can simply be an alias…with some way to disambiguate in the case of Any... and friends.

I don’t understand why functions with variadic parameters can’t have a function that takes an array in place synthesized. What’s the issue with that approach? Doesn’t that just basically automate and eliminate the duplication of effort people have been doing already?

2 Likes

This…actually seems like it could work really well. Even in the Any case, we already have the ability to disambiguate.

Although, there are some slight oddities:

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

func foo(_ x: [Any]) {
  print("array")
}

let x: [Any] = [1]

foo([1])              // variadic
foo([1] as Any)       // variadic
foo([1] as [Any])     // array

foo(x)                // array
foo(x as Any)         // variadic
foo(x as [Any])       // array

Synthesizing an overload would only work if the module the variadic function is in was built with a version of the compiler supporting the feature. Now that we have ABI stability, the standard library and other frameworks have shipped or soon will ship Variadic Functions without synthesized overloads.

Additionally, synthesized overloads wouldn't be able to support closures with varargs. Introducing many new overloads is likely to have a noticeable impact on compile times as well.

1 Like

I think this is somewhat overstated. This already has a defined behaviour, so accomplishing this without breaking source compatibility just means matching that behaviour. Confusion could be handled with targeted warnings, as it is in similar cases (e.g. using an optional in a string interpolation). Variadics are really supposed to be a convenience feature to give users a nicer call site, so I don't find #variadic(…) or as T... to be a great solution here. If you expect users of your API to be regularly using both forms then you're still going to want to provide both overloads. And it still leaves holes in the system, e.g. I don't think it solves problems like this:

func f(_ x: Int...) { … }
func g(_ f: ([Int]) -> ()) { … }
g(f) // error: cannot convert value of type '(Int...) -> ()' to expected argument type '([Int]) -> ()'

without writing a bunch of awkward adapter functions to shuffle the types around.

If this is really the best we can do then it is okay, I guess, because it provides an escape hatch for something that isn't really possible at the moment. But “escape hatch” is the right analogy for it, because it's not going to be pretty and it should only be used in the case of an emergency. Treating variadics solely as syntactic sugar for arrays and making this all work implicitly would be much nicer to use and save library authors from writing forwarding overloads. I'm not yet convinced that this is impossible due to source compatibility, ABI stability or type checking performance requirements. Is it?

5 Likes

The enhanced variadics I pitched sidestep source compatibility issues. I think that is the most viable path to avoiding forwarding overloads.