[SE-0353] constrained existential types behavior

SE-0353 introduced constrained existential types but I couldn't find or understand the reasons of the behavior that I have encountered.

Example

// MARK: - Definitions

protocol Base<T> {
  associatedtype T

  func make() -> T
}

protocol InheritanceExample: Base<Int> {
}

typealias TypeAliasExample = Base<Int>

struct IntBase: InheritanceExample {
    func make() -> Int {
        2
    }
}

// MARK: - Input

func inheritanceAny() -> any InheritanceExample {
    IntBase()
}

func inheritanceSome() -> some InheritanceExample {
    IntBase()
}

func typeAliasAny() -> any TypeAliasExample {
    IntBase()
}

func typeAliasSome() -> some TypeAliasExample {
    IntBase()
}

func baseAny() -> any Base<Int> {
    IntBase()
}

func baseSome() -> some Base<Int> {
    IntBase()
}

// MARK: - Receivers

func acceptInstanceAny(_ base: any Base<Int>) -> Int {
    base.make()
}

func acceptInstanceSome(_ base: some Base<Int>) -> Int {
    base.make()
}

func acceptClosureAny(_ base: () -> any Base<Int>) -> Int {
    base().make()
}

func acceptClosureSome(_ base: () -> some Base<Int>) -> Int {
    base().make()
}

// MARK: - Usage

acceptInstanceAny(inheritanceAny()) // (1) fails with Type of expression is ambiguous without more context
acceptInstanceAny(inheritanceSome())
acceptInstanceAny(typeAliasAny())
acceptInstanceAny(typeAliasSome())
acceptInstanceAny(baseAny())
acceptInstanceAny(baseSome())

acceptInstanceSome(inheritanceAny())
acceptInstanceSome(inheritanceSome())
acceptInstanceSome(typeAliasAny())
acceptInstanceSome(typeAliasSome())
acceptInstanceSome(baseAny())
acceptInstanceSome(baseSome())

acceptClosureAny(inheritanceAny) // (2) fails with Cannot convert value of type '() -> any InheritanceExample' to expected argument type '() -> any Base<Int>'
acceptClosureAny(inheritanceSome)
acceptClosureAny(typeAliasAny)
acceptClosureAny(typeAliasSome)
acceptClosureAny(baseAny)
acceptClosureAny(baseSome)

acceptClosureSome(inheritanceAny) // (3) fails with Type 'any InheritanceExample' cannot conform to 'Base'
acceptClosureSome(inheritanceSome)
acceptClosureSome(typeAliasAny) // (4) fails with Type 'any TypeAliasExample' (aka 'any Base<Int>') cannot conform to 'Base'
acceptClosureSome(typeAliasSome)
acceptClosureSome(baseAny) // (5) fails with Type 'any Base<Int>' cannot conform to 'Base'
acceptClosureSome(baseSome)

Question #1

I guess (1) and (2) fail because any InheritanceExample existential type is not the same existential type as any Base<Int>.
Although, it seems like the compiler could infer that the Base protocol is inherited by InheritanceExample protocol and the constrained associated type is the same.
After all, that's the relevant part.
Could it be fixed, or is that nontrivial?

Question #2

I think (3), (4), and (5) fail because

existential types do not conform to their protocols
Existentials and View - #14 by lukasa

As the error says: Type (...) cannot conform to 'Base'
And some works like a generic parameter so it requires a concrete type conforming to a protocol, but if that's the case, why these work?

acceptInstanceSome(inheritanceAny())
acceptInstanceSome(typeAliasAny())
acceptInstanceSome(baseAny())

Is that a special case where existentials do conform? If so it should work for closures as the return type is covariant.
Is this expected?

Looks like a bug (or really a new feature, but a very reasonable one) in the erased type computation, do you mind filing an issue with this part of the test case?

These work because we open the existential when you pass an existential argument to a generic function parameter. https://github.com/apple/swift-evolution/blob/main/proposals/0352-implicit-open-existentials.md

1 Like

Looks like a bug (or really a new feature, but a very reasonable one) in the erased type computation, do you mind filing an issue with this part of the test case?

Sure, in apple/swift or elsewhere?

These work because we open the existential when you pass an existential argument to a generic function parameter. https://github.com/apple/swift-evolution/blob/main/proposals/0352-implicit-open-existentials.md

Would it be possible to open them for the return type of a closure as well?

This isn't possible in the general case because a function which itself accepts a function parameter f of type () -> some P may rely on the guarantee that the concrete type of the value returned by f is the same for every invocation. OTOH, a function with type () -> any P is permitted to return values having different concrete types on each invocation. Consider:

protocol P: Equatable {}
struct S: P {}
struct R: P {}

func compare(_ f: () -> some P) -> Bool {
  let val1 = f()
  let val2 = f()
  return val1 == val2
}

func createAnyP() -> any P {
  if Bool.random() {
    S()
  } else {
    R()
  }
}

func createSomeP() -> some P {
  S()
}

compare(createAnyP) // error: how could you (possibly) compare 'S' and 'R'?
compare(createSomeP) // ok: 'createSomeP' guaranteed to return same type always
2 Likes

Makes sense :+1: Thanks!