Obviously I'm a huge +1 for this feature, but I have some concerns and
questions about the particulars.
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
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`:
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.
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:
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:
// 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:
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:
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:
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:
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.
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:
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.
···
on Wed Sep 28 2016, Joe Groff <swift-evolution@swift.org> wrote:
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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution
--
-Dave