Thanks for pushing this discussion forward @angela-laar!
I read back through the old thread to refresh my memory and I find that my opinion hasn't changed too much. It would be really nice if we could mitigate the future source break caused by requiring any
for existentials, but I am not yet convinced that this is the way to do it.
There's two different questions to answer here: is having P
serve as shorthand for some P
right for Swift, and is Swift 6 the right time to make such a change? These questions aren't cleanly separable—it might be that the changed pitch here is right for Swift precisely because of the source break from SE-0335 that we have slated for Swift 6, but the change wouldn't stand on its own otherwise. And of course if the answer to the first question is "no," then the second question is moot. But let me try to elaborate on my current feelings about each of them.
Should P
mean some P
?
I'm sympathetic to the point that often times users 'should' use generics when they use an existential because the latter has the more available syntax. I also mostly agree with Ben's position in the previous thread that explaining to users the difference between some
and any
is easier once the user has actually attempted to use a function in a way that generics don't support.
However, I'm also troubled by the fact that today, generic functions (and opaque type values) do behave differently than 'normal' declarations, and while perhaps the best time for the author to discover those differences is once they try to actually use their functions, I don't know that I buy the same is true for arbitrary clients. One of my hobby horses is maintaining the ability to reason locally about code and declarations, without having to click through multiple levels of documentation. By having some P
look like any other type, we lose the ability to look at a function or property and understand basic things about it's behavior (can I form a reference to this function which takes a parameter of type J
? Can I assign the result of this J
-typed property to this other J
-typed property?)
So while it might be easier, pedagogically, to explain to the user what the difference between some
and any
is once they've actually used the declaration in a way that makes the difference matters, I don't believe this justifies making the language more opaque for users reading code who already understand the difference perfectly well. We have tools like educational notes to (hopefully) provide users with the context and examples they might need to understand why they have to choose between any
and some
. Or, we could just immediately offer a fix-it for some P
alone when users try to use bare P
for minimal friction, and then only expose them to the any
suggestion when they try to use the declaration in a way that would require existentials.
Another downside to consider is all the existing documentation and online discussions using bare P
and explaining the semantics which will suddenly become outdated and in many cases, incorrect. This is related to my discussion below about changing the meaning of existing code, but in some ways worse because even with an intervening language version, this code won't ever have a chance to migrate. IMO it is a better experience to copy code from an online resource and have it no longer compile (with an error message explaining why) than to copy code and have it behave in subtly different ways than described by the source.
Perhaps these downsides don't outweigh the benefits of bare P
as some P
outweighs these costs, but it's still not obvious to me that this change would make things better, even in a vacuum.
Is Swift 6 the right time?
Of course, we have to work within the bounds of the language that we have, and if we are of the mind that we really must remove the meaning of P
as any P
, then it might make sense for us to accept an otherwise sub-optimal solution for the benefit of easing the source compatibility story.
But I'm still not convinced on this front. I will admit that I was surprised to see this passage:
and it took me a moment to understand what you're getting at here—is the idea that because SE-0335 has already 'committed' to the 'no bare P
' allowed source break, this pitch would only be 'making invalid code valid' and therefore claiming itself as source compatible?
I'm not totally on board with this characterization. Since this pitch would ship in the same language version as SE-0335, we have to look at the effect of both combined. There are at least some situations where this proposal would, under Swift 6, cause the behavior of existing code to change:
protocol P {}
protocol Q: P {}
struct S: Q {}
func f(_: any P) {
print("any P")
}
func f<T: Q>(_: T) {
print("some Q")
}
func g(_: Q) {
print("any Q")
}
func g(_: any P) {
print("any P")
}
f(S()) // any P
g(S()) // any Q
Under Swift 6, the above call to g
would begin printing any P
instead of any Q
, and this is a problem that wouldn't arise with the status quo of SE-0335. And users who wish to avoid this sort of issue have no choice but to carefully inspect every use of existentials in their program to make sure that they won't accidentally start behaving differently. I think we need to give some serious thought to such issues before we forge ahead with a solution like this. (And if that thought has already been given it would be great to see it elaborated on in the proposal. )
If we instead allowed for a transition period, we would, yes, have a much larger source break, but it would be entirely mechanical and easily applied by an automated migrator—every use of P
becomes any P
, full stop, and your program behaves exactly the way it did before. The certainty of this migration is very appealing to me even if the volume of changes required is more substantial. (I for one would much prefer making the transition in this manner, in codebases that I maintain.)
I suppose that with the pitched solution we would potentially be able to offer users a choice: either change all uses of P
to any P
, or alternatively leave all your code as-is and let the chips fall where they may. Of course, due to the differences between generic declarations those that use existentials, it's possible users will then have individual cases they'll have to track down and convert to using any
because they were forming a function reference somewhere. Maybe we would be able to automate this to a certain level of polish, but my gut tells me that this approach would on the whole require more manual intervention to make the Swift 6 migration in any reasonably large codebase than would the status quo migration under SE-0335.
Of course, I'm sure I've given less thought to this than the authors and have nothing empirical to back up my gut feeling, so if the authors have already examined the migration problem in detail I would also love to see this built upon in the proposal text!
One last random question:
So, to be clear, this means resilient libraries would be prohibited from using the 'bare P
' shorthand in any public API? I think this ought to be discussed in Detailed design rather than only in the resilience section.