[swift-evolution] [Proposal draft] Conditional conformances

Russ Bishop xenadu at gmail.com
Thu Sep 29 17:05:42 CDT 2016

> On Sep 29, 2016, at 11:12 AM, Douglas Gregor <dgregor at apple.com> wrote:
>> On Sep 28, 2016, at 9:48 PM, Russ Bishop <xenadu at gmail.com <mailto:xenadu at gmail.com>> wrote:
>> What other designs were considered and rejected? It seems like some kind of escape hatch would be preferred if you happen to get into this situation, though you make some really good points about the pitfalls.
> I don’t have a fully-baked alternative proposal—it would probably have to involve some kind of preference rule for picking the “best” set of (consistent!) conformances to satisfy a particular request, introduce a disambiguation syntax for cases where that preference rule does the wrong thing, and some way of teaching the dynamic-casting machinery to do the same thing.

Yeah your description is already sounding like a lot of work :)

>> Just to clarify when you say “bans” do you mean if Wrapped: Equatable & HasIdentity then SomeWrapper is not Equatable, or do you mean you get a compile error because there are two constrained conformances SomeWrapper: Equatable? 
> You get a compile error if there are two conformances of SomeWrapper to Equatable; it doesn’t actually matter whether they are conditional, but people are far more likely to expect to be able to having overlapping conditional conformances.

Just to clarify in my mind, the problem here is that Swift would need runtime machinery to look at Wrapped and select the conformance to Equatable based on whether Wrapped: Equatable or Wrapped: HasIdentity. Is that right? Otherwise with the proposal as written Swift would need to check Wrapped to validate the constraints but once it does there is only one implementation of the conformance to pick from. 

I believe you about the type checker, I’m just naively assuming inserting a table to select the correct conformance isn’t a big cost because you would canonicalize the constraints and being disjoint for any type T there would only ever be one matching entry.

>> What would be the problem with allowing multiple conformances to Equatable so long as the constraints are disjoint 
> From the language perspective, “disjoint” would have to mean that there are requirements that actively conflict, e.g., one extension has “Wrapped.Element == Int” and the other has “Wrapped.Element == String”.

Yes, I was also imagining protocols so long as there is no protocol they share in common except the empty protocol.

> There are implementation issues here deep in the type checker, e.g., because if a given type T can potential conform to a protocol P in multiple ways, it introduces a disjunction in the constraint solver that can push the constraint solver to be Even More Exponential.

That’s Bad ™️

> For me, there’s also the usability issue, and that’s the key argument: the *human* has to reason about these things, too, and it is a whole lot simpler if “does T conform to P?” can only be answered in one way. I don’t think the use cases for having overlapping conformances justify such a drastic increase in complexity across the feature.

Constraints are already introducing some complexity there (which is worth it IMHO). You can’t just answer the question is Array: Equatable? You need to know about T. 

>> or the concrete type only adopts one of the available protocols?
> Unless you assume that you have a fully-determined, closed system where you know about every potential conformance of a concrete type, this isn’t a question that can be answered at compile time.

The problem is importing a random library can immediately introduce breakage when it is a compile error, or worse if both reference a shared library you also import… unless we’re saying extensions of types outside your module are only visible in the declaring module which is pretty restrictive e.g. some UI toolkit extending UIKit/AppKit classes with conveniences, or extending Array to say it CanLayout if elements are views where calling a.layout() tells all the views in the array to layout. In that example neither the views nor Array would be declared in the module doing the extending.

Now let’s say I want to use the swift-protobuf library and I also use GenericSocialMediaService’ SDK that also incorporates swift-protobuf. I’m just imagining what happens when we both try to define extensions. It would be nice if they could declare Array: ProtobufMessage where Element: GSMSEntityProtocol but I was able to provide Array: ProtobufMessage where Element: MyOwnProtocol. 

That said the restrictions can always be relaxed later. I’d rather have this feature without overlapping conformances than not have it.

>>> With conditional conformances, the question of which extension "wins" the implied conformance begins to matter, because the extensions might have different constraints on them. For example:
>>> struct X4<T> { }
>>> extension X4: Q where T: Q { }  // implies conformance to P
>>> extension X4: R where T: R { }  // error: implies overlapping conformance to P
>>> Both of these constrained extensions imply a conformance to P, but the actual P implied conformances to P are overlapping and, therefore, result in an error.
>> If the related P conformance were inherited from conformance to Q or R then the rules would (IMHO) make more sense. Wouldn’t the extra rule you need simply be that either Q or R must provide a complete conformance to P (no mix-n-match)? 
> A conformance to P introduced by the first extension would not be usable by the second extension, because the conformance to P introduced by the first extension might depend on details of “T: Q”… and the second extension can’t assume that “T: Q”.

What I’m saying is that if T: Q then the entire implementation of P must come from the first extension. If T: R then the entire implementation of P must come from the second extension. If T: P then neither extension applies because there are overlapping extensions. Basically if there is any overlap then the compiler only considers explicit extensions that match without ambiguity, otherwise the extension is ignored. 

func takes<Value: P>(value: Value) { }

struct CQ: Q { }
struct CR: R { }
struct CP: P { }

takes(X4<CQ>()) //fine, uses first extension
takes(X4<CR>()) //fine, uses second extension 
takes(X4<CP>()) //error: ambiguous conformance	

This relates to the disjoint discussion above:

extension X4: P where T: P { } //error: ambiguous conformance

Even though there is technically a conformance to P available we just ignore it. Seems like this would be knowable statically by looking at the extensions and the protocol relationships in the constraints?

>> What is the rationale for picking the least specialized extension? That’s not what I would naively expect to happen. If T: R & S then I would expect the more specialized S:R implementation to be preferred, and the explicit R implementation to kick in when T: R. 
> We have to pick the least-specialized extension, because it’s the only one that works. If you pick a more-specialized extension to realize the implied conformance to P, the less-specialized extension doesn’t have a conformance to P that it can rely on.

That’s what I was trying to address by saying no mix-n-match. If you’re going to wonder into overlapping territory you must supply two entirely separate implementations of P, one explicit and one inside the Q extension. (I fully admit that might not help the implementation complexity - I don’t know enough about the type checker to answer that).

> I’ll see about clarifying the proposal here, because the choice of (unique) least-specialized isn’t arbitrary: it’s the only answer that provides a correct result without introducing overlapping conformances.
> 	- Doug

Thanks for your work on this proposal!


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160929/60238e99/attachment.html>

More information about the swift-evolution mailing list