Unexpected inferred associated type of existential

protocol P1<A> {
    associatedtype A
    var a: A { get }
}

func foo<T>(p: any P1<T>)  {
    // a is inferred as T, which is expected.
    let a = p.a 
}

// However, if we constrain the associated type in the P1 protocol.
protocol P1<A> {
    associatedtype A: P2
    var a: A { get }
}
protocol P2 {}

func foo<T>(p: any P1<T>) {
    // a is inferred as P2.
    let a = p.a
}

Is this a bug, or intended?
I think the inferred type should always be the more specific one.


The fix is to make T constrained to P2 explicitly.

func foo<T: P2>(p: any P1<T>) {
    // a is inferred as T, which is expected.
    let a = p.a
}

Maybe the Xcode should provide a warning or fix-it for this situation.

P2 is more specific than T. In the first foo, T is a free type variable. In the second, T is a type variable with an upper bound of P2. Both of these are less specific than P2 itself.

Isn't T, as a concrete type, more specific than P2 as an existential?

Not in the generic implementation, no. It’s a type variable that will be provided with a concrete type at runtime.

Right, T is probably a better choice (because it has strictly more information, which is clear since it can be coerced to any P2), but from the type checker’s perspective, any P2 is a fully-known, concrete type…that happens to be a protocol type. And changing this could be source-breaking, unfortunately—imagine if the local were a var and had another value assigned to it later. (I don’t know if it’s the kind of thing that you could change in a language version without having two separate type checkers, either; you’d have to have a type checker expert weigh in on that.)

I don’t understand your argument. For the purposes of type inference, T is P2. T unifies with P1.A. P1.A’s minimum upper bound is P2. Therefore T unifies with P2.

“Upper bound” is not “unifies with”, as evidenced by

func id<Value: Equatable>(_ value: Value) -> Value {
  return value
}

Maybe your statement is incorrect.
In my understanding, the concrete type of a generic type parameter is known statically at the function's call site.

Are you referring to the type context within id, or the type context of the caller of id? Perhaps I’m abusing terminology by calling this “unification”, but within id’s type context the polytype forall Value . Value isSubtypeOf Equatable must be reduced to a simple monotype. The only candidate is Equatable. (Edit: as @jrose explains, this doesn’t mean that Value unifies with Equatable throughout the entirety of Foo.) In the caller’s context, Value is unified with the static type of the argument.

Yes but this information can’t cross into the implementation of foo. That would make Foo no longer generic, and it is impossible to satisfy in the general case:

protocol P1<A> {
  associatedtype A: P2
  var a: A
}
protocol P2 { }

struct One: P1 {
  struct InnerOne: P2 { var bar: Int }
  var a: InnerOne
}

struct Two: P1 {
  struct InnerTwo: P2 { var quux: String }
  var a: InnerTwo
}

foo(One())
foo(Two())

func Foo<T>(p: P1<T>) {
  // at runtime, this function is invoked once with T == One.InnerOne and once with T == Two.InnerTwo.
  // none of this information is available *statically*.
  // at compile time, all that we know is that T is a subtype of P2.
  // therefore any expression of type T can only be treated as ”something that conforms to P2.”
}

Remember, Swift generics are not textual substitutions like C++ templates are. You can’t write p.quux and rely on SFINAE to avoid a type error. Swift says “no, the only operations available on p are the ones declared by P2.”

This is just not true. Here’s another example:

func meaningless<A: Equatable, B: Equatable>(a: A, b: B) {
  assert(a == b) // error, obviously!
  var local = a
  assert(local == a) // valid
  local = b // error!
}

Static type parameters are not erased just because we only know their bounds. (I’m not sure whether it could work the way you’re describing without Self type requirements, but it doesn’t.)

Aha, now I understand what you’re saying. I’ve been incorrectly generalizing from expressions involving p to the entire type environment of the function.

That said, the part you quoted isn’t actually the wrong part! It’s the part that comes immediately after that’s wrong: the resulting monotype is a new anonymous type that’s a subtype of Equatable, not the Equatable type itself.

1 Like

I suggest you test your example in Xcode, then you will find that the type of T is inferred as One.InnerOne and Two.InnerTwo statically.
Actually, you cannot call a generic function if the concrete types of its type parameters cannot be determined.
For this specific example, the underlying type of p is dynamic, but T should be static.

And I updated my original post with a fix that allows p.a to be inferred as T.

A type parameter cannot be statically inferred as two different types.

respectively for the two foo calls

That’s not what you asked about. You asked about the type of T within body of foo.

That adding the P2 constraint explicitly to the generic parameter list of the function changes the type resolution behavior internally to the function smells like a bug to me… I’d expect that constraint to get inferred anyway from the use of T as primary associated type argument to P1. Seems like something is going a bit wrong with the erasure rules. cc @hborla for after the holidays.

1 Like