SE-0352 "Implicitly Opened Existentials" has a limitation that prevents the opening of an existential argument when the corresponding parameter is optional. For example, this will not open:
func cannotOpen6<T: P>(_ x: T?) { }
func test(pOpt: (any P)?, p1: any P) {
cannotOpen6(pOpt) // does NOT allow opening pOpt
cannotOpen6(p1) // does NOT allow opening p1
}
The rationale in the proposal is not based on a technical argument, but rather a kind of consistency argument:
The case of optionals is somewhat interesting. It's clear that the call cannotOpen6(pOpt) cannot work because pOpt could be nil, in which case there is no type to bind T to. We could choose to allow opening a non-optional existential argument when the parameter is optional, e.g.,
cannotOpen6(p1) // we *could* open here, binding T to the underlying type of > p1, but choose not to
but this proposal doesn't allow this because it would be odd to allow this call but not the cannotOpen6(pOpt) call.
In practice, this limitation is unfortunate, because it means that there is no way to call a generic function with an optional parameter like this without hopping through a method on a protocol extension. The premise of SE-0352 isn't really sound, either: developers can understand that it doesn't make sense to "open up" an optional argument when there might not be anything there (nil), and have a reasonable workaround when they do have an optional argument (e.g., via if let or guard let).
We should remove this restriction, so that cannotOpen6(p1) opens up p1. The implementation is trivial, but alas, I reverted it while writing SE-0352.
This remark made me worry about whether there is a larger âopening + wrappingâ generality problem lurking here, but the protocol extension method trick apparently isnât necessary; this also works in Swift 5.7:
func openOpt<T: P>(_ x: T?) { }
func openNonOpt<T: P>(_ x: T) {
openOpt(x) // allowed, because T is already bound
}
func test(p1: any P) {
openNonOpt(p1) // allowed, because weâre not jumping inside an optional
}
Removing that middle step for optionals does raise the question of whether things like this should also work:
Yes, your understanding is correct. And I agree that this is a good place for a context-specific diagnostic.
One minor issue is that your declaration of foo had an unintended any that could be confusing. It should look like this:
func foo<T>(arg: T?) { }
Right, the "middle step" here isn't dealing with existentials at all, so there's nothing to open. It's an "optional injection", i.e., the equivalent of .some(x).
Yeah, that's an interesting point. The way SE-0352 specifies the semantics here is that we type-erase the result of .success(p1), so we'd a Result<any P, Error>. To generalize here would mean delaying the type erasure of the result to the outer call. I'm not sure what the natural end for this would be... if we had a double-wrapped .some(.success(p1)) would that delay all the way through the chain?
I think it's an interesting direction, but it does take my mini-pitch from being a small correction into a more significant extension.
This took me a moment, but yes, you sure do have a point: Iâm describing a wildly more complicated idea than your proposal. Thinking out loud to check my own understanding of what youâre saying, my two-step âworks in 5.7â process up above for opening an existential into an optional param is equivalent to this:
âŚand it makes sense to automatically fold that whole openNonOptional step into compiler magic, because Swift already has a special policy about automatically wrapping values in Optional.some. If we conceptualize your proposal as automatically generating my openNonOptional function here, thereâs no ambiguity about what to generate.
But there is no equivalent policy about automatically wrapping values in .success, so the direction Iâm proposing requires one of the following:
Allowing enums to specify a âdefault caseâ of some kind, and then using that to generalize Swiftâs optional wrapping to other enum types. Then the middle step above can be well-defined even if we just pass p1directly for a param of type Result<T,Error>, without wrapping it in .success at the call site.
Doing something sneaky in the type system to defer the type erasure of .success(p1), as you described, so that .success(p1) can move inside the implicit openNonOptional function as it were.
Option 1 seems interesting to me. IIRC, it was already discussed and soft-rejected, though?
In any case, I completely support keeping the scope of the proposal at hand nice and narrow. Itâs an entirely worthy and sensible fix as is.
+1. Makes sense. We shouldn't ask developers to write an extension method to do this. Nullable parameters are important for opaque types so they will exist.
I don't think the (1) you're proposing has been discussed on its own. There were discussions on generalized implicit conversions that could have allowed this (but don't seem likely to succeed), and older discussions around letting other types benefit from optional's syntactic sugar. Personally, I'd rather not add a conversion here, but...