Generic type doesn't use specialised protocol implementation for associated type

Sorry for the poor title. I don't know how to describe the problem succinctly. Hopefully the following code example illustrates it better:

protocol P {
	associatedtype Bar
	var bar: Bar { get }
	var typeOfBar: String { get }
	func printTypeOfBar()
}

extension P {
	var typeOfBar: String { "Not Int" }
	func printTypeOfBar() {
		print(typeOfBar)
	}
}

extension P where Bar == Int {
	var typeOfBar: String { "Int" }
}

struct Foo<Bar>: P {
	let bar: Bar
}

let foo = Foo(bar: 42)

print(foo.typeOfBar) // prints "Int"
foo.printTypeOfBar() // prints "Not Int"

In the example above, I expected foo.printTypeOfBar() to print "Int", because foo is known at compile time to be of type Foo<Int>, and P has specialised implementation for when Bar == Int.

My understanding with protocol witness and dynamic dispatch has always been fuzzy. It would be great if someone could explain this behaviour for me.

I think my question has some similarity to one from 4 months ago, but the older question seems more about extensions to generic types, not protocols:

1 Like

In Swift, a generic type conforms to a protocol in exactly one way. The way that it conforms is determined statically at compile-time, where the conformance is declared.

A “conformance” to a protocol means, essentially, a lookup table to identify the implementation satisfying each of the protocol’s requirements.

So at the point where Foo is declared as conforming to P, the compiler constructs a table to specify, for each requirement of P, which member of Foo satisfies that requirement.

This is a single table for Foo, which does not depend on the generic parameter Bar. Every instance of Foo conforms to P in the same way, using the same table.


At the point where Foo conforms to P, there is only one available implementation for the bar requirement, namely the stored property in Foo.

There is only one available implementation for the printTypeOfBar requirement, namely the function in the unconstrained extension P.

And there is also only one available implementation for the typeOfBar requirement, namely the computed property in the unconstrained extension P.

The constrained extension is not available for all possible instances of Foo, so the version of typeOfBar found there cannot be the implementation that goes in the conformance table. The conformance table must work for every instance of Foo.

That means Foo conforms to P by using the version of typeOfBar from the unconstrained extension.


When you write foo.typeOfBar, the compiler performs static dispatch on the instance foo. It sees two possible implementations for typeOfBar: one from the unconstrained extension on P, and one from the constrained extension.

The compiler chooses the version from the constrained extension, since it is a closer match to the type of foo. That is why you see “Int” as the first output.


When you write foo.printTypeOfBar(), the compiler still performs static dispatch on the instance foo. It sees only one possible implementation, from the unconstrained extension on P.

Inside the body of printTypeOfBar, however, the call to typeOfBar uses dynamic dispatch through the protocol requirement. This is because the implementation is located in an extension of P, where the only thing known about Self is that it conforms to P.

So printTypeOfBar calls the version of typeOfBar found in the conformance table for Foo, which as we saw earlier is the one from the unconstrained extension on P.

That is why you see “Not Int” as the second output.

3 Likes

Thank you! I think my mental model for protocol conformance is a lot clearer now!

I'm curious about this part. If printTypeOfBar uses dynamic dispatch to find the preferred version typeOfBar, then it suggests that there is a way to have more than 1 version of typeOfBar in the conformance table?

Not at all, there is only one version of each protocol requirement in the conformance lookup table. There could be many different types conforming to the protocol (that’s kind of the point of protocols) each with their own conformance table containing their own implementations of each requirement.

Dynamic dispatch means that the conformance table is used at runtime to find the implementation of a given requirement for a given conforming type.

(Or at least it must behave as if it looked in the table for the conforming type at runtime. When everything is visible at compile-time there could be inlining or other optimizations, but the actual function that’s called must be the same one that’s in the conformance table for that type.)


In Swift there are three ways to achieve dynamic dispatch (meaning the call is chosen at runtime):

Manual casting
You can use as? to attempt casting to different types at runtime, and call the desired implementation based on which cast succeeds.

Subclass overrides
With a variable whose static type is one class, but whose dynamic type is a subclass, calling a member of the superclass will dynamically at runtime call the override of that member provided by the subclass.

Protocol requirements
With a variable whose static type is either a protocol existential (aka. a value “of protocol type”) or a placeholder type constrained to conform to the protocol, then requirements of that protocol are dynamically dispatched at runtime for that variable:

// Protocol existential
var p: P
// Requirements of `P` are dispatched dynamically on `p`

// Generic placeholder
func f<T: P>(_ t: T) {
  // Requirements of `P` are dispatched dynamically on `t`
}

// Self placeholder
extension P {
  // Requirements of `P` are dispatched dynamically on `self`
}

// Associated type
protocol Q {
  associatedtype A: P
  var a: A
}
// Anywhere that requirements of `Q` are dispatched dynamically on an instance `q`,
// then requirements of `A` are dispatched dynamically on `q.a`
3 Likes