My belated review:
I’m a +1, and delighted to see this longstanding language gap filled at last. Still, it seems to me this proposal is necessary but not sufficient, so I hope the work does not end here.
Several things about the proposal originally gave me pause:
- It’s a bit…magic-y. Is it too magic? Will people be surprised when they accidentally encounter the feature? Does the magic introduce undesired behavior?
- Forcing a new function to open an existential limits refactoring options / expressivity, and doesn’t compose nicely with other language features. Couldn’t we do some lexically scoped version of this?
- Is the feature discoverable? When people need to unwrap an existential, how do they realize they need to pull the code that works with the concrete type into its own function? That is a curious leap.
Reading the proposal and the discussion, and doing my own fumbling experiments, I’m convinced on point 1: yes, it is a bit magical to have a type invisibly transform like this from caller to callee, but the behavior of that magic (1) is almost always going to be what people actually wanted, to the extent they won’t notice it’s magic, and (2) the magic almost never shadows other behavior people would want instead. To the extent it’s magical, it’s also desirable and intuitive.
Points 2 and 3 still stick in my craw. It is a basic building block of Alogol’s many descendants that it is generally possible to inline a function in source, as it were:
a
f(…)
d a
—becomes→ b
func f(…) { c
b d
c
}
Under this proposal, a function that opens an existential is no longer thus inlinable. By forcing the creation of new functions that may interrupt the logic flow of code and may not have a meaningful name, this proposal will thus create a scattering of fragmentary functions. The situation is akin to how Objective-C’s target/selector approach to callbacks could leave classes littered with tiny methods that represented the tail ends of thoughts started elsewhere, much like reading a Choose Your Own Adventure book straight through out of order.
Swift’s nested functions mitigate this problem, though the resulting dance is hardly ideal:
let foo: any P
...
openFoo(foo)
func openFoo(_ foo: some P) {
...
}
Worried about all that, this sentence from the proposal put me at peace with its approach as a good starting point:
Opening an existential means looking into the existential box to find the dynamic type stored within the box, then giving a "name" to that dynamic type. That dynamic type name needs to be captured in a generic parameter somewhere, so it can be reasoned about statically, and the value with that type can be passed along to the generic function being called.
Yes, right, we need a way to name that type that resulted from the opening, or the opening isn’t worth much — and right now, Swift can only introduce generic types at function or type boundaries. Any sort of solution that offers the inlining I’m looking for necessitates a large increase in language surface area, whereas the proposal’s answer makes an urgently needed feature available with minimal impact. I’m convinced. +1, good proposal, yes, do it.
I do hope we don’t stop there, however. This proposal leaves a gap in the shape of the undocumented _openExistential
. (Aside: I think it is not possible to fully recreate the behavior of _openExistential
for a generic protocol P
using this proposal, even in Swift 6 mode, because there is no way to specify type parameters <P, T:P>
in Swift. Is that correct?)
Edit: I misremembered _openExistential
as accepting a closure. I was picturing this nonexistent beast:
let foo: any Widget
...
openExistential(foo) { fooOpened in
...
}
…Or better yet, I like the alternative/future direction the proposal mentions of allowing opening via coercion from any P
to some P
. The introduction of opaque result types introduced the problem of types that are unnameable, which IIRC greatly vexed both @Chris_Lattner3 and me during the review of that proposal. Being able to refer to a specific some
type would open up this future direction:
let foo: any Widget // where P has associated type Doodad
...
let fooOpened: some Widget = foo
let bar: TypeOf(fooOpened).Doodad = fooOpened.doodad // bad hypothetical syntax
…and this would grant my “descendants of Algol” wish from above. I do hope we head in that direction eventually.