An Implementation Model for Rational Protocol Conformance Behavior

I'm talking about the witness table itself, i.e., the mapping the protocol requirements to specific witnesses that implement those requirements. At the point where (say) Array is defined to conform to Collection, we can record additional information about what other potential witnesses existed (but didn't apply) and choose among them.

I believe there is enough leeway there to address SR-12881 in a reasonable manner, if we capture---at the point of DefaultIndices conformance to Collection---the potential set of distance(from:to) witnesses for that conformance (one in the extension of Collection, another in the extension of BidirectionalCollection, a third in RandomAccessCollection) and delay the decision of which witness to use until we have a specific concrete generic argument type (e.g., DefaultIndices<ClosedRange<Int>>. That can probably be made to line up with what overload resolution would do for a call to DefaultIndices<ClosedRange<Int>>(...).distance(from:to:) when we have fully concrete types at type-checking time.

Are those the semantics you want?

Note that, due to retroactive conformances, there may be more than one witness table for a given conformance of a type X to a protocol P. Those witness tables may have different witnesses (of course), and both may be used at run time. This can result in having two types that would be spelled the same way in source code actually be distinct because they use different conformances. Let's make this concrete:

// Module A
public struct X { }

// Module B
import A
public extension X: Hashable  { ... }

public func getBSet() -> Set<X> { ... }

// Module C
import A
public extension X: Hashable  { ... }

public func getCSet() -> Set<X> { ... }

The types Set<X> produced by modules B and C are distinct, because they use different conformances of X: Hashable. The Swift runtime will keep them distinct, with different type metadata pointers (each of which includes the appropriate witness table).

The Swift compiler has known problems with this if you import both B and C into a fourth module, e.g.,

// Module D
import A
import B
import C

func test() {
  let setX1 = getBSet()
  let setX2 = getCSet()
  if setX1 == setX2 { ... }  // SHOULD be an compile-time error, probably won't be, might crash in runtime
}

PR #31895 is a step toward fixing this, but this is important: having multiple retroactive conformances affects type identity.

@jrose makes an excellent point about as? being tricky, which is what I brought up the above:

Let's say I have code like this:

func test(any: Any) {
  if let setOfX = any as? Set<X> { ... }
}

And somehow I end up feeding the results of getBSet() and getCSet() into it. What do we expect to happen? Does your answer change depending on which module defines the test(any:) function?

Another example:

func testHashable(any: Any) {
  if let h = any as? Hashable { /* assume Hashable doesn't have a Self requirement */ }
}

Now arrange for an instance of X to get passed in here. What do we expect to happen? Does your answer change depending on which module defines the testHashable(any:) function?

As those discussions and this one have shown, implementation concerns weigh heavily here, because a lot of what people abstractly want are not implementable in an efficient and/or backwards-compatible manner. That doesn't mean we should drive the discussion from the implementation, though. We have to know what actually semantics we want, and then the push-pull of design and implementability can kick in.

I'm unable to map from your statement of intention and implementation sketch to the semantics you want. Answering my bolded questions above would help me understand the goal, then I can tell you what is achievable w.r.t. implementation. You answered some similar questions in Ergonomics: generic types conforming "in more than one way" - #72 by dabrahams, but we're in a different thread and these kinds of semantic decisions haven't been recorded/explained anywhere. They really need to be, because I don't think anyone can understand the intended semantics without these kinds of small examples to reason through.

Doug

13 Likes