Conditional conformances

Proposal: SE-NNNN 
<https://github.com/DougGregor/swift-evolution/blob/conditional-conformances/proposals/NNNN-conditional-conformances.md>
Author: Doug Gregor <https://github.com/DougGregor>
Review Manager: TBD
Status: Awaiting review
During the review process, add the following fields as needed:

Decision Notes: Rationale <https://lists.swift.org/pipermail/swift-evolution/>, 
Additional Commentary <https://lists.swift.org/pipermail/swift-evolution/>
Bugs: SR-NNNN <https://bugs.swift.org/browse/SR-NNNN>, SR-MMMM 
<https://bugs.swift.org/browse/SR-MMMM>
Previous Revision: 1 
<https://github.com/apple/swift-evolution/blob/...commit-ID.../proposals/NNNN-filename.md>
Previous Proposal: SE-XXXX 
<https://github.com/DougGregor/swift-evolution/blob/conditional-conformances/proposals/XXXX-filename.md>
 
<https://github.com/DougGregor/swift-evolution/tree/conditional-conformances#introduction>Introduction

Conditional conformances express the notion that a generic type will conform to 
a particular protocol only when it's type arguments meet certain requirements. 
For example, the Array collection can implement the Equatable protocol only 
when its elements are themselves Equatable, which can be expressed via the 
following conditional conformance on Equatable:

extension Array: Equatable where Element: Equatable {
  static func ==(lhs: Array<T>, rhs: Array<T>) -> Bool { ... }
}
This feature is part of the generics manifesto 
<https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#conditional-conformances->
 because it's something that fits naturally into the generics model and is 
expected to have a high impact on the Swift standard library.

Swift-evolution thread: TBD: Discussion thread topic for that proposal 
<https://lists.swift.org/pipermail/swift-evolution/>
 
<https://github.com/DougGregor/swift-evolution/tree/conditional-conformances#motivation>Motivation

Conditional conformances address a hole in the composability of the generics 
system. Continuing the Array example from above, it's always been the case that 
one could use the == operator on two arrays of Equatable type, e.g., [Int]() == 
[Int]() would succeed. However, it doesn't compose: arrays of arrays of 
Equatable types cannot be compared (e.g.,[Int] 
<https://github.com/DougGregor/swift-evolution/blob/conditional-conformances/proposals>==
 [Int] 
<https://github.com/DougGregor/swift-evolution/blob/conditional-conformances/proposals>will
 fail to compile) because, even though there is an==for arrays of 
Equatabletype, the arrays themselves are neverEquatable`.

Conditional conformances are particularly powerful when building generic 
adapter types, which are intended to reflect the capabilities of their type 
arguments. For example, consider the "lazy" functionality of the Swift standard 
library's collections: using the lazy member of a sequence produces a lazy 
adapter that conforms to the Sequence protocol, while using the lazy member of 
a collection produces a lazy adapter that conforms to the Collection protocol. 
In Swift 3, the only way to model this is with different types. For example, 
the Swift standard library has four similar generic types to handle a lazy 
collection: LazySequence, LazyCollection, LazyBidirectionalCollection, and 
LazyRandomAccessCollection. The Swift standard library uses overloading of the 
lazy property to decide among these:

extension Sequence {
  var lazy: LazySequence<Self> { ... }
}

extension Collection {
  var lazy: LazyCollection<Self> { ... }
}

extension BidirectionalCollection {
  var lazy: LazyBidirectionalCollection<Self> { ... }
}

extension RandomAccessCollection {
  var lazy: LazyRandomAccessCollection<Self> { ... }
}
This approach causes an enormous amount of repetition, and doesn't scale well 
because each more-capable type has to re-implement (or somehow forward the 
implementation of) all of the APIs of the less-capable versions. With 
conditional conformances, one can provide a single generic wrapper type whose 
basic requirements meet the lowest common denominator (e.g., Sequence), but 
which scale their capabilities with their type argument (e.g., the LazySequence 
conforms to Collection when the type argument does, and so on).

 
<https://github.com/DougGregor/swift-evolution/tree/conditional-conformances#proposed-solution>Proposed
 solution

In a nutshell, the proposed solution is to allow a constrained extension of a 
struct, enum, or class 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:

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:

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.

 
<https://github.com/DougGregor/swift-evolution/tree/conditional-conformances#detailed-design>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.

 
<https://github.com/DougGregor/swift-evolution/tree/conditional-conformances#disallow-overlapping-conformances>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:

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:

No, it does not conform because T is neither Equatable nor HasIdentity.
Yes, it conforms via the first extension of SomeWrapper because T conforms to 
Equatable.
Yes, it conforms via the second extension of SomeWrapper because T conforms to 
HasIdentity.
Ambiguity, because T conforms to both Equatable and HasIdentity.
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:

// 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:

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

protocol P { }

struct S : P { }
extension S : P { } // error: S already conforms to P
 
<https://github.com/DougGregor/swift-evolution/tree/conditional-conformances#implied-conditional-conformances>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:

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"

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:

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.

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. 
Continuing the example from above:

protocol S: R { }

struct X5<T> { }

extension X5: R where T: R { }  // "wins" implied conformance to P, because
extension X5: S where T: S { }  // the extension where "T: S" is more 
specialized
                                // than the one 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:

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, or
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. 
 
<https://github.com/DougGregor/swift-evolution/tree/conditional-conformances#overloading-across-constrained-extensions>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:

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

 
<https://github.com/DougGregor/swift-evolution/tree/conditional-conformances#source-compatibility>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.

 
<https://github.com/DougGregor/swift-evolution/tree/conditional-conformances#effect-on-abi-stability>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:

Improve composability: the example in the introduction 
<https://github.com/DougGregor/swift-evolution/blob/conditional-conformances/proposals/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.
Eliminating repetition: the lazy wrappers described in the motivation 
<https://github.com/DougGregor/swift-evolution/blob/conditional-conformances/proposals/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.
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. 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.

 
<https://github.com/DougGregor/swift-evolution/tree/conditional-conformances#alternatives-considered>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:

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.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to