[swift-evolution] [Review] SE-0143: Conditional Conformances

Dave Abrahams dabrahams at apple.com
Fri Sep 30 00:16:17 CDT 2016


Obviously I'm a huge +1 for this feature, but I have some concerns and
questions about the particulars.

on Wed Sep 28 2016, Joe Groff <swift-evolution at swift.org> wrote:

> Hello Swift community,
>
> The review of “Conditional Conformances” begins now and runs through October 7. The proposal is available here:
>
> 	https://github.com/apple/swift-evolution/blob/master/proposals/0143-conditional-conformances.md
> Reviews are an important part of the Swift evolution process. All
> reviews should be sent to the swift-evolution mailing list at
>
> https://lists.swift.org/mailman/listinfo/swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>
> or, if you would like to keep your feedback private, directly to the
> review manager. When replying, please try to keep the proposal link at
> the top of the message:
>
> Proposal link: 
>
> https://github.com/apple/swift-evolution/blob/master/proposals/0143-conditional-conformances.md


> ## Proposed solution
>
> In a nutshell, the proposed solution is to allow a constrained
> extension of a `struct`, `enum`, or `class` (but [not a
> protocol](#alternatives-considered)) to declare protocol
> conformances. No additional syntax is necessary for this change,
> because it already exists in the grammar; rather, this proposal
> removes the limitation that results in the following error:
> 
> ```
> t.swift:1:1: error: extension of type 'Array' with constraints cannot have an inheritance clause
> extension Array: Equatable where Element: Equatable { }
> ^                ~~~~~~~~~
> ```
> 
> Conditional conformances can only be used when the additional
> requirements of the constrained extension are satisfied. For example,
> given the aforementioned `Array` conformance to `Equatable`:
> 
> ```swift
> func f<T: Equatable>(_: T) { ... }
> 
> struct NotEquatable { }
> 
> func test(a1: [Int], a2: [NotEquatable]) {
>   f(a1)    // okay: [Int] conforms to Equatable because Int conforms to Equatable
>   f(a2)    // error: [NotEquatable] does not conform to Equatable because NotEquatable has no conformance to Equatable
> }
> ```
> 
> Conditional conformances also have a run-time aspect, because a
> dynamic check for a protocol conformance might rely on the evaluation
> of the extra requirements needed to successfully use a conditional
> conformance. For example:
> 
> ```swift
> protocol P {
>   func doSomething()
> }
> 
> struct S: P {
>   func doSomething() { print("S") }
> }
> 
> // Array conforms to P if it's element type conforms to P
> extension Array: P where Element: P {
>   func doSomething() {
>     for value in self {
>       value.doSomething()
>     }
>   }
> }
> 
> // Dynamically query and use conformance to P.
> func doSomethingIfP(_ value: Any) {
>   if let p = value as? P {
>     p.doSomething()
>   } else {
>     print("Not a P")
>   }
> }
> 
> doSomethingIfP([S(), S(), S()]) // prints "S" three times
> doSomethingIfP([1, 2, 3])       // prints "Not a P"
> ```
> 
> The `if-let` in `doSomethingIfP(_:)` dynamically queries whether the
> type stored in `value` conforms to the protocol `P`. In the case of an
> `Array`, that conformance is conditional, which requires another
> dynamic lookup to determine whether the element type conforms to `P`:
> in the first call to `doSomethingIfP(_:)`, the lookup finds the
> conformance of `S` to `P`. In the second case, there is no conformance
> of `Int` to `P`, so the conditional conformance cannot be used. The
> desire for this dynamic behavior motivates some of the design
> decisions in this proposal.

Whether a dynamic evaluation is required at this point seems to depend
on how you represent conformance.  Saying “Array conforms conditionally”
treats Array as a type, but it might be simpler to treat Array as a
family of concrete types.  I always envisined it this way: at the moment
a module using the concrete type Array<Foo> comes together with an
extension (either in that module or in another) that makes Array<T>
conform to Equatable depending on properties of T, that extension is
evaluated and Array<Foo> is marked as conforming or not.  Then asking
about a type's conformance is always a simple thing.  But I suppose
which approach wins is dependent on many factors, and the other way can
be thought of as adding lazy evaluation to the basic model, so if this
adds nothing to your thinking please excuse the static.

> ## Detailed design
>
> Most of the semantics of conditional conformances are
> obvious. However, there are a number of issues (mostly involving
> multiple conformances) that require more in-depth design.
> 
> ### Disallow overlapping conformances
> With conditional conformances, it is possible to express that a given
> generic type can conform to the same protocol in two different ways,
> depending on the capabilities of its type arguments. For example:
> 
> ```swift
> struct SomeWrapper<Wrapped> {
>   let wrapped: Wrapped
> }
> 
> protocol HasIdentity {
>   static func ===(lhs: Self, rhs: Self) -> Bool
> }
> 
> extension SomeWrapper: Equatable where Wrapped: Equatable {
>   static func ==(lhs: SomeWrapper<Wrapped>, rhs: SomeWrapper<Wrapper>) -> Bool {
>     return lhs.wrapped == rhs.wrapped
>   }
> }
> 
> extension SomeWrapper: Equatable where Wrapped: HasIdentity {
>   static func ==(lhs: SomeWrapper<Wrapped>, rhs: SomeWrapper<Wrapper>) -> Bool {
>     return lhs.wrapped === rhs.wrapped
>   }
> }
> ```
> 
> Note that, for an arbitrary type `T`, there are four potential answers to
> the question of whether `SomeWrapper<T>` conforms to `Equatable`:
> 
> 1. No, it does not conform because `T` is neither `Equatable` nor
> `HasIdentity`.
> 2. Yes, it conforms via the first extension of `SomeWrapper` because
> `T` conforms to `Equatable`.
> 3. Yes, it conforms via the second extension of `SomeWrapper` because
> `T` conforms to `HasIdentity`.
> 4. Ambiguity, because `T` conforms to both `Equatable` and
> `HasIdentity`.

Arguably in this case you could say yes it conforms and it doesn't
matter which extension you use because there's only one sensible
semantics for Equatable.  Other protocols are less obvious, though
(c.f. Int's conformance to Monoid with +/0 vs */1).

> It is due to the possibility of #4 occurring that we refer to the two
> conditional conformances in the example as *overlapping*. There are
> designs that would allow one to address the ambiguity, for example, by
> writing a third conditional conformance that addresses #4:
> 
> ```swift
> // Possible tie-breaker conformance
> extension SomeWrapper: Equatable where Wrapped: Equatable & HasIdentity, {
>   static func ==(lhs: SomeWrapper<Wrapped>, rhs: SomeWrapper<Wrapper>) -> Bool {
>     return lhs.wrapped == rhs.wrapped
>   }
> }
> ```
> 
> The design is consistent, because this third conditional conformance
> is more *specialized* the either of the first two conditional
> conformances, meaning that its requirements are a strict superset of
> the requirements of those two conditional conformances. However, there
> are a few downsides to such a system:
> 
> 1. To address all possible ambiguities, one has to write a conditional
> conformance for every plausible combination of overlapping
> requirements. To *statically* resolve all ambiguities, one must also
> cover nonsensical combinations where the two requirements are mutually
> exclusive (or invent a way to state mutual-exclusivity).
>
> 2. It is no longer possible to uniquely say what is required to make a
> generic type conform to a protocol, because there might be several
> unrelated possibilities. This makes reasoning about the whole system
> more complex, because it admits divergent interfaces for the same
> generic type based on their type arguments. 

I'm pretty sure we already have that.  Array<T> would have hashValue if
T were hashable, and < if T were comparable, and never the twain shall
meet.

> At its extreme, this invites the kind of cleverness we've seen in the
> C++ community with template metaprogramming, which is something Swift
> has sought to avoid.
>
> 3. All of the disambiguation machinery required at compile time (e.g.,
> to determine whether one conditional conformance is more specialized
> than another to order them) also needs to implements in the run-time,
> as part of the dynamic casting machinery. One must also address the
> possibility of ambiguities occurring at run-time. This is both a sharp
> increase in the complexity of the system and a potential run-time
> performance hazard.
> 
> For these reasons, this proposal *bans overlapping conformances*
> entirely. While the resulting system is less flexible than one that
> allowed overlapping conformances, the gain in simplicity in this
> potentially-confusing area is well worth the cost. Moreover, this ban
> follows with existing Swift rules regarding multiple conformances,
> which prohibit the same type from conforming to the same protocol in
> two different ways:
> 
> ```swift
> protocol P { }
> 
> struct S : P { }
> extension S : P { } // error: S already conforms to P
> ```

I think that's the real answer to “why don't we want to allow it?”

Also, these overlapping constraints are effectively equivalent to “or”
constraints, which I think we know are trouble, from a type checking
POV.

[By the way, what is the status of our enforcement of this rule across
modules?  I do think that library interop will eventually oblige us to
allow different overlapping conformances to appear in separate modules
that don't also define the conforming type].

> ### Implied conditional conformances 
> 
> Stating conformance to a protocol implicitly states conformances to
> any of the protocols that it inherits. This is the case in Swift
> today, although most developers likely don't realize the rules it
> follows. For example:
> 
> ```swift
> protocol P { }
> protocol Q : P { }
> protocol R : P { }
> 
> struct X1 { }
> struct X2 { }
> struct X3 { }
> 
> extension X1: Q { }  // implies conformance to P
> 
> extension X2: Q { }  // would imply conformance to P, but...
> extension X2: P { }  // explicitly-stated conformance to P "wins"

What difference does it make which one “wins?”  Even if P had
requirements and we were fulfilling them in these extensions, my mental
model has always been that they get thrown into a big soup and we let
overload resolution sort it out (or declare ambiguity).  So I've never
had any idea that “winning” was happening.

> extension X3: Q { }  // implies conformance to P
> extension X3: R { }  // also implies conformance to P
>                      // one will "win"; which is unspecified
> ```
> 
> 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:
> 
> ```swift
> 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.

Doesn't this break, then?

   protocol Equatable { ... }
   protocol Comparable : Equatable { ... }
   protocol Hashable : Equatable { ... }

   extension Array : Comparable where T : Comparable {}
   extension Array : Hashable where T : Hashable {}

I hope not, beause personally, I don't see how breaking the ability to
do things like that could be acceptable.  The protocols such as
Equatable et al. that form the components of “Regular” types are *going*
to be inherited by many other protocols, so this isn't just going to
happen in a nice localized context, either.

> However, in cases where there is a reasonable ordering between the two
> constrained extensions (i.e., one is more specialized than the other),
> the less specialized constrained extension should "win" the implied
> conformance. 

“Less specialized wins” is surprising at the very least!  I've only ever
seen the opposite rule (e.g. in overload resolution).  What's the
rationale for this approach?

> Continuing the example from above:
> 
> ```swift
> protocol S: R { }
> 
> struct X5<T> { }
> 
> extension X5: S where T: S { }
> 
> // This last extension "wins" the implied conformance to P, because
> // the extension where "T: R" is less specialized than the one
> // where "T: S".
> extension X5: R where T: R { }
> ```
>
> Thus, the rule for placing implied conformances is to pick the *least
> specialized* extension that implies the conformance. If there is more
> than one such extension, then either:
> 
> 1. All such extensions are not constrained extensions (i.e., they have
> no requirements beyond what the type requires), in which case Swift
> can continue to choose arbitrarily among the extensions, 

Surely that can't be the right long-term design?

> or
> 
> 2. All such extensions are constrained extensions, in which case the
> program is ill-formed due to the ambiguity. The developer can
> explicitly specify conformance to the protocol to disambiguate.

I'm really concerned about the understandability of this model.  It
doesn't seem rational or consistent with other choices in the
language. Now maybe I'm missing something, but in that case I think you
haven't given a clear/precise enough description of the rules.  But the
next section looks like it might clear things up for me...

...(I'm sorry to say it didn't)

> ### Overloading across constrained extensions
> 
> One particularly important aspect of the placement rule for implied
> conformances is that it affects which declarations are used to satisfy
> a particular requirement. For example:
> 
> 
> ```swift
> protocol P {
>   func f()
> }
> 
> protocol Q: P { }
> protocol R: Q { }
> 
> struct X1<T> { }
> 
> extension X1: Q where T: Q {           // note: implied conformance to P here
>   func f() {
>     // #1: basic implementation of 'f()'
>   }
> }
> 
> extension X1: R where T: R {
>   func f() {
>     // #2: superfast implementation of f() using some knowledge of 'R'
>   }
> }
> 
> struct X2: R {
>   func f() { }
> }
> 
> (X1<X2>() as P).f() // calls #1, which was used to satisfy the requirement for 'f'
> X1<X2>().f()        // calls #2, which is preferred by overload resolution
> ```
> 
> Effectively, when satisfying a protocol requirement, one can only
> choose from members of the type that are guaranteed to available
> within the extension with which the conformance is associated. 

AFAICT, this is the first time that an extension's association to a
conformance has ever been visible in the user model, and I am not sure
that we want to expose it.  I'm *not* saying we *don't* want to; I'm
saying it isn't obvious why we should.

> In this case, the conformance to `P` is placed on the first extension
> of `X1`, so the only `f()` that can be considered is the `f()` within
> that extension: the `f()` in the second extension won't necessarily
> always be available, because `T` may not conform to `R`. 

I don't see how that's any different from the `f()` in the first
extension, which won't necessarily always be available because `T` may
not conform to `Q`.

> Hence, the call that treats an `X1<X2>` as a `P` gets the first
> implementation of `X1.f()`. When using the concrete type `X1<X2>`,
> where `X2` conforms to `R`, both `X.f()` implementations are
> visible... and the second is more specialized.
> 
> Technically, this issue is no different from surprises where (e.g.) a
> member added to a concrete type in a different module won't affect an
> existing protocol conformance. 

Let me be absolutely clear that I'm *not* questioning this scheme on
those grounds.  I think it makes sense for static and dynamic (or
generic) behaviors of the same spelling to differ sometimes, and it's
even a useful design tool (c.f. someCollection.lazy).  However, the fact
that it arises in some situations and is even sometimes the best design
choice doesn't mean that's the way *this* feature should act in *this*
situation.  I can't tell whether you're suggesting:

a) type safety demands this behavior
b) it's the only behavior that's efficiently implementable
c) it's actually the best behavior for the common use cases, or
d) something else

> The existing ideas to mediate these problems---warning for
> nearly-matching functions when they are declared in concrete types,
> for example---will likely be sufficient to help surprised users. That
> said, this proposal may increase the likelihood of such problems
> showing up.
> 
> ## Source compatibility
> 
> From the language perspective, conditional conformances are purely
> additive. They introduce no new syntax, but instead provide semantics
> for existing syntax---an extension that both declares a protocol
> conformance and has a `where` clause---whose use currently results in
> a type checker failure. That said, this is a feature that is expected
> to be widely adopted within the Swift standard library, which may
> indirectly affect source compatibility.
> 
> ## Effect on ABI Stability
> 
> As noted above, there are a number of places where the standard
> library is expected to adopt this feature, which fall into two
> classes:
> 
> 1. Improve composability: the example in the
> [introduction](#introduction) made `Array` conform to `Equatable` when
> its element type does; there are many places in the Swift standard
> library that could benefit from this form of conditional conformance,
> particularly so that collections and other types that contain values
> (e.g., `Optional`) can compose better with generic algorithms. Most of
> these changes won't be ABI- or source-breaking, because they're
> additive.
> 
> 2. Eliminating repetition: the `lazy` wrappers described in the
> [motivation](#motivation) section could be collapsed into a single
> wrapper with several conditional conformances. A similar refactoring
> could also be applied to the range abstractions and slice types in the
> standard library, making the library itself simpler and smaller. All
> of these changes are potentially source-breaking and ABI-breaking,
> because they would remove types that could be used in Swift 3
> code. However, there are mitigations: generic typealiases could
> provide source compatibility to Swift 3 clients, and the ABI-breaking
> aspect is only relevant if conditional conformances and the standard
> library changes they imply aren't part of Swift 4.

I think the description “eliminating repetition” does a disservice to
the actual impact of this feature.  It will result in a massive decrease
in API surface area, which simplifies the implementation of the library,
yes, but also makes it far more accessible to users.

> Aside from the standard library, conditional conformances have an
> impact on the Swift runtime, which will require specific support to
> handle dynamic casting. If that runtime support is not available once
> ABI stability has been declared, then introducing conditional
> conformances in a later language version either means the feature
> cannot be deployed backward or that it would provide only more
> limited, static behavior when used on older runtimes. 

It would also mean carrying forward all that implementation and API
complexity for the forseeable future.

> Hence, there is significant motivation for doing this feature as part
> of Swift 4. Even if we waited to introduce conditional conformances,
> we would want to include a hook in the runtime to allow them to be
> implemented later, to avoid future backward-compatibility issues.
> 
> ## Effect on Resilience
> 
> One of the primary goals of Swift 4 is resilience, which allows
> libraries to evolve without breaking binary compatibility with the
> applications that use them. While the specific details of the impact
> of conditional conformances on resilience will be captured in a
> more-complete proposal on resilience, possible rules are summarized
> here:
> 
> * A conditional conformance cannot be removed in the new version of a
> library, because existing clients might depend on it.

I think that applies to all conformances.

> * A conditional conformance can be added in a new version of a
> library, roughly following the rules described in the [library
> evolution
> document](https://github.com/apple/swift/blob/master/docs/LibraryEvolution.rst#new-conformances). The
> conformance itself will need to be annotated with the version in which
> it was introduced.

Presumably this is an ABI-preserving but potentially source-breaking
change, because of the implied conformance ambiguity issues you
mentioned earlier... and because of changing overload resolution, etc.?

> * A conditional conformance can be *generalized* in a new version of
> the library, i.e., it can be effectively replaced by a (possibly
> conditional) conformance in a new version of the library that is less
> specialized than the conditional conformance in the older version of
> the library. For example.
> 
>   ```swift
>   public struct X<T> { }
>   
>   // Conformance in version 1.0
>   public extension X: Sequence where T: Collection { ... }
>   
>   // Can be replaced by this less-specialized conformance in version 1.1
>   public extension X: Sequence where T: Sequence { ... }
>   ```
>   
>   Such conformances would likely need some kind of annotation.

Ditto?

> ## Alternatives considered
> 
> The most common request related to conditional conformances is to allow a (constrained) protocol extension to declare conformance to a protocol. For example:
> 
> ```swift
> extension Collection: Equatable where Iterator.Element: Equatable {
>   static func ==(lhs: Self, rhs: Self) -> Bool {
>     // ...
>   }
> }
> ```
> 
> This protocol extension will make any `Collection` of `Equatable`
> elements `Equatable`, which is a powerful feature that could be put to
> good use. Introducing conditional conformances for protocol extensions
> would exacerbate the problem of overlapping conformances, because it
> would be unreasonable to say that the existence of the above protocol
> extension means that no type that conforms to `Collection` could
> declare its own conformance to `Equatable`, conditional or otherwise.

...which is part of why I think we need better rules about overlap than
the ones proposed here.  I anticipate a repetition of the pattern we
have today, where Array provides == and != but can't conform to
Equatable, just on a different level.  That has always felt wrong.  Now
we'll do the same thing with Collection, so *at least* you don't have to
write the == operator when you write the conditional conformance.
 
> There are several potential solutions to the problem of overlapping
> conformances (e.g., admitting some form of overlapping conformances
> that can be resolved at runtime or introducing the notion of
> conformances that cannot be queried a runtime), but the feature is
> large enough to warrant a separate proposal that explores the
> solutions in greater depth.

I think the problem and solutions need to be explored in greater depth
now.

>
> What is your evaluation of the proposal?
> Is the problem being addressed significant enough to warrant a change to Swift?
> Does this proposal fit well with the feel and direction of Swift?
> If you have used other languages or libraries with a similar feature,
> how do you feel that this proposal compares to those?
> How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
> More information about the Swift evolution process is available at
>
> https://github.com/apple/swift-evolution/blob/master/process.md
> <https://github.com/apple/swift-evolution/blob/master/process.md>
> Thank you,
>
> -Joe
>
> Review Manager
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>

-- 
-Dave



More information about the swift-evolution mailing list