+1, I'd love to see something like this happen. FWIW, on the bikeshed, I also love your proposal of a #keyword
style syntax for this. Whether it is #variadic
or #splat
or something else can be discussed in the review thread: I'd recommend collecting a bunch of alternatives and listing them in the proposal.
Hi all,
I've updated the draft with a few minor edits, and a new section on alternate spellings for the feature. The original post and gist should both be up-to-date.
Are there any future directions? Do we eventually want to deprecate the current T...
syntax and use something a little more verbose but more flexible like variadic Set<Int>
or variadic [Int]
? That would be a good counter part to the #variadic(...)
directive.
Not really - it's not about "beyond T…
"; my point is that we could replace T…
completely and have a simpler yet more powerful language. Maybe a language that isn't backwards compatible anymore, but as long as the people have a wrong impression about alternatives, I guess they still haven't been discussed enough.
Another try for to explain briefly
T…
would be replaced with just [T]
- not only behind the scenes as it is now, but also in source. To resolve some possible problems, it could also be something like @variadic [T]
instead, but that's no fundamental difference.
I'll call the option where all array parameters would accept variadics "implicit" and the alternative where this would only happen when an annotation is used "explicit"
Comparison
I made a small table to compare several options — to keep it simple, I only classified aspects as good () or not so good (
) without further differentiation.
I didn't choose the aspects in the matrix to make any option look especially bad, but the numbers speak for themselves:
Maybe I forgot some rows, but I'm pretty sure to have a good explanation for every bad rating of the status quo — and my personal opinion is that relative importance of all the aspects does not favor T...
either.
Status quo | Pitch | Implicit variadics | Explicit variadics | No variadics | |
---|---|---|---|---|---|
Complexity (declaration) | ![]() |
![]() |
![]() |
![]() |
![]() |
Complexity (call site) | ![]() |
![]() |
![]() |
![]() |
![]() |
Convenience (declaration) | ![]() |
![]() |
![]() |
![]() |
![]() |
Convenience (call site) | ![]() |
![]() |
![]() |
![]() |
![]() |
Ambiguity | ![]() |
![]() |
![]() |
![]() |
![]() |
Complexity (forwarding) | ![]() |
![]() |
![]() |
![]() |
![]() |
Convenience (forwarding) | ![]() |
![]() |
![]() |
![]() |
![]() |
Flexibility | ![]() |
![]() |
![]() |
![]() |
![]() |
Imho "no variadics" is especially interesting:
Is it really a good tradeoff to have a magical type, and now also add an even more magical macro just to get rid of two tiny brackets at the call site?
Many people never use a variadic function besides print
, and it would be simple to add dozens of variants with different arity (and afaik, something similar has just been proposed for SwiftUI).
Yes, I know — many readers here are in love with variadics, but imho you should at least try to look at them from a purely pragmatical standpoint before you add even more magic to support them.
What is supposed to happen with:
foo(bar: #variadic([]))
The issue I've had few times in the past with a variadic parameter is that it cannot have a default value as below so that I cannot omit a parameter:
func foo(bar: Int... = []) {...}
Given the following function:
func foo(bar: Int...) {...}
We can already call it today with no variadic arguments as foo()
. foo(bar: #variadic([]))
is equivalent.
Yes, really. Think about it: What problem are you trying to solve? What problem is this proposal trying to solve? Do they match up?
I think you're missing the two most important—and most objective—rows:
| | Status Quo | Pitch | Implicit variadics | Explicit variadics | No variadics |
| - | - | - | - | - | - | - |
| Binary compatible | |
|
|
|
|
| Source compatible | |
|
|
|
|
I don't think a major redesign is, or should be, on the table for this.
Could you be explicit about why and where implicit variadics (see edit) allowing variadic functions to also accept arrays would be source and/or binary incompatible? Is it impossible to maintain source compatibility or does it just lead to somewhat confusing behaviour when you deliberately write a bunch of Any...
/[Any]
overload trick questions?
A clear explanation and set of examples here would make the proposal much stronger, because the one example in the proposal doesn't seem to break source compatibility and just requires matching existing behaviour. Perhaps this is buried in one of the many old threads? Or is this obvious to everyone but me?
Edit: And I'll point out that you can already write similar riddles for yourself with existing features to while away the time or make variadics look like a bad idea:
func f(_ x: Any...) { print("variadic: \(x)") }
func f(_ x: [Any]) { print("array: \(x)") }
let x = [1, 2, 3]
f(x) // variadic: [[1, 2, 3]]
func g<T>(_ x: T...) { print("variadic: \(x)") }
func g<T>(_ x: [T]) { print("array: \(x)") }
g(x) // array: [1, 2, 3]
Edit 2: I now see that “implicit variadics” in @Tino's post is something new entirely, where any array parameter can be called like a variadic, not the “Implicitly convert Array
s when passed to variadic arguments” discussed in the proposal as an alternative. I'm interested in understanding why that isn't possible and/or desirable.
In addition to the Any...
ambiguity, which I think itself is enough to rule out any kind of implicit conversion approach, overload ranking would be another major issue. Consider this example:
protocol P {}
protocol Q: P {}
protocol R: Q {}
func foo(_ x: [P]) { print("p") }
func foo(_ x: R...) { print("r") }
let y: [R] = []
foo(y) // Which overload is called?
If implicit conversion is introduced, func foo(_ x: R...)
becomes the more specific overload and is called instead of func foo(_ x: [P])
, breaking source compatibility.
Trying to paper over these incompatibilities with special-case default behavior would add significant complexity to the type checker. It would also result in an extremely confusing set of typing rules for variadic arguments, because the behavior would be defined by avoiding source incompatibilities as opposed to predictability and consistency. This is not a feature I'd expect users to reach for on a daily basis, so it shouldn't have wildly unintuitive behavior they need to remember when they do encounter it.
I don't think the fact that the existing behavior can sometimes be confusing should be used to justify introducing even more complexity.
Thanks for the example. I don't think this is a given, though:
because it seems like you could consider the array to be a more specific match, as it already is in my g<T>
example above. I don't find that to be “extremely confusing” or “wildly unintuitive” on its own. Does it become all of those things in conjunction with the other rules that would be required to avoid a source compatibility break?
My point here was that I don't think that deliberately trying to invent confusing examples is a good way to evaluate a language feature. It wasn't to say that “confusion already exists so we should deliberately create more”.
I think that's pretty much exactly what would happen. We already have two separate cases which demonstrate the need for specific, obscure type-checking rules. A real implementation of implicit conversions would require at least a few more, and I'm not even convinced it would be possible to implement in a fully correct manner.
But are they specific or obscure? For these examples so far it seems to me that you can construct very similar examples that are already implemented in the shipping language. e.g. similar to your recent example:
protocol P {}
protocol Q: P {}
protocol R: Q {}
struct S: R { }
func f(_ x: P) { print("P") }
func f(_ x: R...) { print("R") }
func g(_ x: P, _ y: P) { print("P") }
func g(_ x: R...) { print("R") }
let y: R = S()
f(y) // Which overload is called?
g(y, y) // Which overload is called?
Not identical, but in a similar spirit. So it's not clear to me that the rules here don't just roughly follow from how things already work.
I mean, "implicitly convert Array
s when passed to variadic arguments" would literally change the output of print(myArray)
. That isn't an obscure riddle; people print all sorts of stuff all the time and we've deliberately designed the representation of printed arrays to be useful for debugging. And if you flip the default so that arrays never splat into Any...
, then you can't always get the splatting behavior when you want it.
I don't think we can get away with splatting arrays into variadic arguments without some explicit indication in the source that the user wants us to do it.
Everyone already accepts that Any
would have to be an exception or treated differently, as it already is currently. Is it the only exception? I accept your good point that it means that there would remain no way to get splatting behaviour when dealing with Any
, which is unfortunate (though people are getting by currently with no way to do splatting at all). If this tradeoff is mentioned in the pitch then that's fair. My thoughts are still:
There is nothing special about Any
here. The ambiguity exists whenever [T]
is a subtype of T
. Specifically, when T
is a protocol which Array
could conform to, or a generic parameter which could represent Array
.
Sure, it's accepted that to maintain 100% source compatibility then situations like these need to be disambiguated in a way that matches current behaviour, whatever it is. If you try to write down the current rules for variadics and how overloads, etc are ranked (or really anything that needs ranking in the constraint system) they're not exactly straightforward, either. It's just still not particularly clear to me if this would be somehow uniquely complicated. But we're kind of going in circles here.
Edit: And, I mean, the required rule seems to mostly be “prefer not to do the conversion from [T]
to T...
, which falls in line with current rules you can read out of the ranking file like:
- Prefer the more specialized protocol
- Prefer the member in the concrete type
- Prefer members of subprotocols over those of the protocols they inherit, if all else fails
- If both are class properties with the same name, prefer the one attached to the subclass
- Prefer the catamorphism (flattening) over the mplus (non-flattening) overload if all else is equal
- etc.
Again, I'm not convinced that it's possible or a good idea, I'm just equally not convinced by the proposal that it's impossible or a bad idea.
I don't think that is a compelling example: The only difference is that there wouldn't be any commas in the output, and the brackets would be missing in the console as well.
How important can it be that an array thrown at stdout without any explanation about its meaning appears in the logs unchanged? I don't think calls like print(myArray)
should be used in production code at all, but even if it does, a change in formatting wouldn't do real harm.
But you are pitching to add a huge piece of complexity:
There's already magic to express that a list of arguments should be treated as an array, and the change would add a "counter spell" for that to express that you want "normal" behavior.
And what's the benefit of all the special syntax? Nothing but a tiny bit of convenience, because you don't have to typ [
and ]
.
Compare that to a design you only have [T]
(and no T…
) and add a small bit of sugar to allow skipping the brackets at call site:
There wouldn't be magic types for variadics, and you wouldn't need splatting at all.
Ambiguity wouldn't exist either (at least for the compiler), because arrays would always be treated as arrays.
Pitch | Alternate reality |
---|---|
print(#variadic(myArray)) |
print(myArray) |
print(myArray) |
print([myArray]) |
print("Some explanation", myArray) |
print("Some explanation", myArray) |
Instead of using something completely new (technically, macro might not even be the right name for #variadic
) when you want splatting, you would use a well established and very concise syntax to express that you don't want it.
I still can't see any beauty in the T...
/ #variadic
solution, and I hate when this aspect is sacrificed on the altar of compatibility.
Maybe Swift is already in the phase where we have no choice but keep piling up features (and cruft), but I personally really wouldn't be angry about breaking changes as long as they improve the language (and don't happen as frequently as in the early days ;-).
The pitch wants to add splatting, whereas the design that I prefer has no need for splatting at all.
How can "get rid of X" and "add new features which are exclusive for X" be orthogonal?
Thanks for doing the hard work to implement this <3
For whatever it's worth, I prefer #splat
.