[swift-evolution] [Proposal draft] Conditional conformances

Douglas Gregor dgregor at apple.com
Fri Sep 30 00:41:21 CDT 2016


> On Sep 29, 2016, at 3:05 PM, Russ Bishop <xenadu at gmail.com> wrote:
> 
> 
>> On Sep 29, 2016, at 11:12 AM, Douglas Gregor <dgregor at apple.com <mailto: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?


Correct. And diagnose / disambiguate if there are multiple conformances that match.

> 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. 

Right.

> 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.

The table computation would have to be a runtime thing, because we don’t know all of the conformances until then, but yes—it’s doable.

>> 
>>> 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.

The compiler won’t know that a given type can’t conform to two specific protocols, though, unless they have some kind of direct conflict (like my example above of Wrapped.Element being equated to two different concrete types) or we have some mechanism in the language to state that two protocols are mutually exclusive.

>>> 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. 

Yes, I know.

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

Right. If we find a model for it that works.

> 
> 
>> 
>>>> 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.

This fails when T: Q&R, though, because you don’t know which implementation of P to choose—the one based on T: Q or the one based on T: R? That’s the reason for completing banning the overlap—it avoids the possibility of ambiguity (at compile time or at runtime).

> 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	

In the model you describe, this last one isn’t “ambiguous conformance”, it’s “no conformance” because neither of the (potential) conformances of X4 to P has its requirements satisfied. The case you didn’t enumerate is:

struct CQR: Q, R { }
takes(X4<CQR>()) // 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.

I don’t understand your “error: ambiguous conformance” annotation here. This extension of X4 is less specialized than either of the other extensions, yet provides a suitable P conformance, and (if it were present in your example immediately above) would allow “takes(X4<CP>())” to succeed. Indeed, this is the fix for the example in SE-0143 because it eliminates the overlap by providing a common, shared conformance to P.

>  Seems like this would be knowable statically by looking at the extensions and the protocol relationships in the constraints?


You can see that either of the first two extensions is more specialized than this third extension, if you can see them all. Dynamic work comes in when multiple modules with extensions are linked together.

>>> 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).

If we were to allow overlap, then yes, maybe it should have to be explicit. My complaints/concerns about overlapping conformances still apply ;)

	- Doug

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


More information about the swift-evolution mailing list