[Returned for revision] SE-0352: Implicitly Opened Existentials

Hi everyone. The review of SE-0352: Implicitly Opened Existentials ran from April 5…April 18, 2022. The core team has decided to return the proposal for revision. Although discussion was almost unanimously positive, and largely consisted of clarifying questions, there is one important design decision to clarify regarding the behavior of covariantly-erased return types in the face of other language evolution. Given the following example:

protocol P {
   associatedtype A: Collection<Int>
}
func foo<T: P>(x: T) -> T.A { ... }

func bar(_: some Collection) { print("comme ci") }
func bar(_: some Collection<Int>)  { print("comme ça") }

func callFooWithAnyP(x: any P) {
  let y = foo(x)
  bar(y)
}

then under the language today, the best type-erased result type from the call to foo we can come up with today would be any Collection, which would then be inferred for the type of y. However, if the language gains more expressive existentials in the future, such as same-type constrained existential types, then we could conceivably infer a narrower upper bound type for y, such as any Collection<Int>, which can affect overload resolution in source-breaking ways. In the above example, improving the type erasure behavior would cause bar(y) to select the second overload of bar instead of the first.

We want to be able to continue to improve the expressivity of existentials without breaking existing code, but we want the return values of functions called with opened existentials to be as useful as possible. The core team would like to see the proposal undergo another round of review to explore this design space. This isn't technically a new problem, since as soon as SE-0309 allowed all protocols to be used in existential types, it was possible to create similar situations using return types involving associated types, but SE-0352 makes the problem more pervasive, and we want to make sure we have a satisfying consistent answer to this problem going forward. The proposal for same-type constrained existential types is also ready for review concurrently, providing a timely concrete example of expanding the expressivity of existential types, and a chance to consider its interaction with covariant erasure when opening existentials.

Aside from that issue, there was also some discussion of whether the proposal should retain the ability to open existentials more freely within a function, such as by binding them explicitly to a variable with an opaque type like let x: some P = existentialP and/or leaving the results of function calls with opened existentials open and unerased for further processing. The authors removed these aspects from the proposal out of concerns for adding too much complexity to the language all at once. Future proposals could still introduce local existential opening.

Thanks to everyone who participated in the review!

20 Likes

I don’t have input on the review right now, but I want to address a meta-point.

If the example is from a properly designed API, then the two versions of bar should do the same thing. The only difference should be that the more-specialized bar might use a better-optimized algorithm.

In that sense, fundamentally, I think “This change might cause overload resolution to select a more-specific candidate” should not be considered API breaking.

1 Like

I think the biggest concern is with propagating type inference:

func f<T: Collection>(_ val: T) { return val }
let a = [1, 2, 3]
var b = f(a)
b = ["a", "b", "c"] // succeeds if f(a) returns any Collection, fails if f(a) returns any Collection<Int>
3 Likes

You won't get any defense of overloading from me, and you're right in principle, but we don't want to rely on the good behavior of API designers if we can help it. As @ksluder noted, overload resolution is also just one possible effect.

3 Likes

Wait—why would it fail?

Because you’re trying to assign an Array<String> to a variable whose type was inferred to be any Collection<Int>.

:man_facepalming: Of course; I misread the example.

Great! I enjoy reading the proposal reviews, so the more the merrier!

Typo here: "April 5…April 8, 2022"

-snip-

Could it limit the upper bound to be no more specific than the original argument?

func f<T: Collection>(_ val: T) -> T { return val }
let a: any Collection = [1, 2, 3]        // A
let a: any Collection<Int> = [1, 2, 3]   // B
var b = f(a)
b = ["a", "b", "c"] // succeeds for A, fails for B

If so, I would not find it surprising, although this is just a toy example.

func f<T: Collection>(_ val: T) -> T.Element? { return val.first }
let a: any Collection = [1, 2, 3]
var b = f(a)
b = ... // Any-thing at all ✅
2 Likes