An Implementation Model for Rational Protocol Conformance Behavior

Yes, it's the same things. In the example with P, Q, and X<T>. I think expanding the example is useful to illustrate the "conforms in one way" bit, e.g., add

struct NotEquatable { }

print( callId( X<NotEquatable>() ) ) // "1"

When the conformance of X<T>: P is established, there is only a single definition of id that is guaranteed to work for any T: the one on the extension of P. So that one gets picked, and will be the same regardless of what T ends up being substituted with. The conformances for any X<T> to P behave uniformly.

Y'all have found override and @_nonoverride. One can use @_nonoverride to introduce a new requirement in a more-refined protocol, which then selects a witness on its terms. Let's extend the example to introduce a non-overriding id requirement in Q.

protocol P { var id: Int { get } }
extension P { var id: Int { 1 } }

protocol Q: P {
  @_nonoverride var id: Int { get } // introduces a unique requirement `id`
}
extension Q { var id: Int { 2 } }

struct X<T>: P {}
extension X: Q where T: Equatable {}

func callIdAsP<T: P>(_ t: T) -> Int { t.id } // dispatches through `P.id`
func callIdAsQ<T: Q>(_ t: T) -> Int { t.id } // dispatches through `Q.id`

print( callIdAsP( X<Int>() ) ) // "1"
print( callIdAsQ( X<Int>() ) ) // "2"

In the conformance of X<T>: P, the id defined in the extension of P is the only viable witness that works for any T. Hence, the requirement P.id is satisfied by the id defined in the extension of `That's the same answer we had in the prior example.

In the conformance of X<T>: Q, both the id defined in the extension of P and the id defined in the extension of Q can be used to satisfy the requirement Q.id. The id defined in the extension of Q is more specific, so that is used to satisfy the requirement Q.id.

These two choices are made independently.

Now, to the uses. In callIdAsP, the only way to type-check t.id is to go through the requirement P.id, so we get "1" in the call given X<Int>.

In callIsAsQ, the expression t.id can either call through the requirement P.id or the requirement Q.id. Overload resolution picks Q.id, because it is more specific, so we get "2" in the call given X<Int>.

We can change the semantics. That might need to be staged in with a version bump (e.g., Swift 6), but I wouldn't get too caught up with the "when" part of this. Figure out what the model should be.

The model I've been imagining would collect the set of potential witnesses at the point where the conformance was declared, but delay the selection of the "best" witness to satisfy a given requirement until enough information is available---whether that happens at compile time or at run time.

For example, rewind back to the conformance X<T>: P. The id defined in the extension of P is a viable witness that works for any T. The id defined in the extension of Q is potentially viable; it depends on whether the actual type substituted for T conforms to Equatable and, if so, it is more specific than the id defined in the extension of P. Hence, we would need to delay the decision of the "best" witness until we have a concrete type substituted in for T. For T=Int, Int conforms to Equatable so the id defined in the extension of Q will be selected as the witness. For T=NotEquatable, the id defines in the extension of P will be selected as the witness.

For the conformance X<T>: Q, both the iddefined in the extension ofPand theiddefined in the extension ofQare viable, and the one in the extension ofQis better, so the one in the extension ofQ` is selected as the witness. There is no reason to delay the decision until concrete types are available, because the decision won't change.

All of the examples written/mentioned above would print 2 for X<Int>, regardless of how they are called. And 1 for X<NotEquatable>, of course.

In this model, @_nonoverride can probably remain a hidden relic, needed only to maintain ABI for the Standard Library where it is used.

The shape of a protocol's witness table is established by the protocol itself; each type that conforms to a protocol must provide a witness table that matches that shape. The protocol Q from the original example does not contain an entry for id; only P contains such an entry, because P declares the requirement id. What you describe matches up with the @_nonoverride trick I describe above.

There are 135 messages in this thread, 65 in this thread, and 67 in this thread.

Doug (just one me)

2 Likes