[swift-evolution] [Proposal draft] Conditional conformances

plx plxswift at icloud.com
Sun Oct 2 10:22:28 CDT 2016

> On Sep 30, 2016, at 1:23 PM, Douglas Gregor <dgregor at apple.com> wrote:
>> This is purely anecdotal but I had a lot of utility code laying around that I’d marked with notes like `// TODO: revisit once conditional conformances are available`.
>> When I was leaving those notes I was expecting to need overlapping conformances often, but I reviewed them *before* replying and I actually haven’t found an example where having overlapping conformances is both (1) a significant win and also (2) a win in a way that’d be of broad, general interest.
>> - 80% have no real need for overlapping conditional conformances
>> - 15% might have “elegance gains” but nothing practically-significant
>> - 5% would *probably* see real gains but are likely not of broad interest
>> …which wasn’t what I was expecting, but leaves me a lot more comfortable without overlapping conformances for now than I was in the abstract.
> Very interesting, thanks for doing this review!

I've taken the time to provide a bit more color on the 80/15/5 breakdown because I don't see much discussion for this proposal in terms of concrete situations...just theoretical concerns and theoretical possibilities. I don't have any completed code either, but I have notes and to-do lists for things I was planning to do, and I think seeing even some semi-concrete scenarios might be helpful here.

The "80%" are generally analogous to the `SomeWrapper` in the writeup; as a concrete example, I was waiting on the availability of conditional conformances to resume work on an emulation of structural unions, e.g. something like:

  enum Sum2<A,B> {
    case a(A)
    case b(B)
...(and analogously for 3, 4, as-necessary). 

There's a very obvious way to write `extension Sum2 : Equatable where A:Equatable, B:Equatable {}`...and at the time I set this aside, I was expecting to also want to come back and have additional conformances for things like `...where A:Equatable, B:AnyObject` (using `===` for comparing `B`) and so on for other combinations.

Upon revisiting such things in light of the proposal, I now think differently: for this case it seems like a better long-term approach anyways to stick to a single conformance and work with it like this:

  extension Sum2:Equatable where A:Equatable, B:Equatable {
    // details elided
  /// Adaptor using `ObjectIdentifier` to implement `==`.
  struct ObjectWrapper<Wrapped:AnyObject> : Equatable, Hashable {
    let wrapped: Wrapped
...as upon reflection I really would prefer dealing with the hassle of working with `Sum2<A,ObjectWrapper<B>>` in situations where -- in theory -- `Sum2<A,B>` could do -- to the hassle of writing out 4+ conformances for `Sum2` (and so on...even with nice code-gen tools that's going to be a lot of bloat!). 

What changed my mind was tracing through the implications of conditional conformances for the use-site ergonomics of adaptors like `ObjectWrapper` above; what I mean is, suppose I have a protocol like this:

  protocol WidgetFactory {
    associatedtype Widget
    associatedtype Material
    func produceWidget(using material: Material) -> Widget

...then it's rather easy to simply write this type of boilerplate:

  extension ObjectWrapper: WidgetFactory where Wrapped: WidgetFactory {
    typealias Widget = Wrapper.Widget
    typealias Material = Wrapper.Material
    func produceWidget(using material: Material) -> Widget {
      return base.produceWidget(using: material)
...which thus means I have the tools I need to make my use of wrappers like the `ObjectWrapper` largely transparent at the use sites I care about; e.g. I can write a single conditional conformance like this:

  extension Sum2: WidgetFactory 
    A:WidgetFactory, B:WidgetFactory,
    A.Material == B.Material,
    A.Widget == B.Widget {
    typealias Widget = A.Widget
    typealias Material = A.Material
    func produceWidget(using material: Material) throws -> Widget {
      switch self {
        case let .a(aa): return aa.produceWidget(using: material)
        case let .b(bb): return bb.produceWidget(using: material)
...and it will apply even in situations where circumstances left me using `ObjectWrapper` (or similar) on any of the type parameters to `Sum2` (e.g. if I also needed an `Equatable` conformance for whatever reason).

At least for now--when I'm still just revisiting plans and thinking about it in light of the proposal--I really would prefer having a simpler language and writing this type of boilerplate, than having a more-complex language and writing the *other* type of boilerplate (e.g. the 4+ `Equatable` conformances here, and so on for other situations).

Note that I'm not claiming the above is the only use for overlapping conditional conformances -- not at all! -- just that situations like the above comprise about 80% of the things I was intending to do with conditional conformances...and that after revisiting them expecting to be troubled by the proposed banning of overlapping conformances, I'm now thinking I'd wind up not using the overlapping-conformance approach in such cases even if it were available.

So that's the first 80%.

Moving on, the next 15% are places where there's some forced theoretical or aesthetic inelegance due to the lack of overlapping conformances, but none of these seem to have any significant practical import.

A representative case here is that I currently have the following pair:

  /// `ChainSequence2(a,b)` enumerates the elements of `a` then `b`.
  struct ChainSequence2<A:Sequence,B:Sequence> : Sequence
    where A.Iterator.Element == B.Iterator.Element  {
    // elided

  /// `ChainCollection2(a,b)` enumerates the elements of `a` then `b`.
  struct ChainCollection2<A:Collection,B:Collection> : Collection
    where A.Iterator.Element == B.Iterator.Element {
    // ^ `where` is not quite right, see below

...and obviously conditional conformances will allow these to be consolidated into a single `Chain2` type that then has appropriate conditional conformances (and for which the cost/benefit for me will tip in favor of adding conditional conformances to `BidirectionalCollection` and `RandomAccessCollection`, also).

On paper--e.g., theoretically--the lack of overlapping conformances leaves in a few aesthetic issues...for example, at present `ChainCollection2` actually has to be one of these:

  // "narrower" option: not all `A`, `B` can necessarily be used together:
  struct ChainCollection2<A:Collection,B:Collection> : Collection
    A.Iterator.Element == B.Iterator.Element,
    A.IndexDistance == B.IndexDistance {
    typealias IndexDistance = A.IndexDistance

  // "wasteful" option: theoretically in some cases we are "overpaying" and 
  // using a stronger `IndexDistance`, but now we can use any `A` and `B`
  struct ChainCollection2<A:Collection,B:Collection> : Collection
    where A.Iterator.Element == B.Iterator.Element {
    typealias IndexDistance = IntMax

With overlapping conditional conformances you could have both: one conformance that uses base collections' `IndexDistance` when possible, and another that uses `IntMax` when necessary...but without conditional conformances it's necessary to choose between the "narrower" approach or the "wasteful" approach (preserving the status quo).

If you're following along I'm sure you're aware that in this specific case, this "choice" is purely academic (or purely aesthetic)...if you go with the `IntMax` route there's almost always going to be between "no actual difference" and "no measurable difference", so even if it *maybe* feels a bit icky the right thing to do is get over it and stop making a mountain out of an anthill.

Note that I'm well aware that you can choose to see this as a concrete instance of a more-general problem -- that the lack of overlapping conformances would potentially leave a lot of performance on the table due to forcing similar decisions (and in contexts where there *would* be a real difference!) -- but speaking personally I couldn't find very much in my "chores pending availability of conditional conformance" that both (a) fell into this category and (b) had more than "aesthetic" implications. 

This brings me to that last 5% -- the handful of things for which overlapping conformances have nontrivial benefits -- and my conclusion here is that these tended to be things I doubt are of general interest.

An example here is that I like to use a function that takes two sequences and enumerates their "cartesian product", with the following adjustments:

- no specific enumeration *ordering* is guaranteed
- does something useful even with infinite, one-shot sequences...
- ...meaning specifically that it will eventual-visit any specific pair (even when one or both inputs are infinite, one-shot)

...(useful for doing unit tests, mostly), which to be done "optimally" while also dotting all the is and crossing all the ts would currently require at least 8 concrete types:

- 4 sequences, like e.g.:
  - UnorderedProductSS2<A:Sequence, B:Sequence>
  - UnorderedProductSC2<A:Sequence, B:Collection>
  - UnorderedProductCS2<A:Collection, B:Sequence>
  - UnorderedProductCC2<A:Collection, B:Collection>
- 4 iterators (one for each of the above)

...since you need to use a different iteration strategy for each (yes you don’t *need* 8 types, but I’m trying to “dott all is, cross all ts” here). 

In theory overlapping conditional conformances could be used to cut that down to only 5 types:

- 1 type like `UnorderedProduct<A:Sequence,B:Sequence>`
- the same 4 iterators from before, each used with the appropriate conformance

...which *is* less code *and* seemingly provides nontrivial gains (the `SS` variant must maintain buffers of the items it's already seen from each underlying sequence, but the others have no such requirement).

But, to be honest, even if those gains are realized, this is the kind of situation I'm perfectly comfortable saying is a "niche" and neither broadly relevant to the majority of Swift developers nor broadly relevant to the majority of Swift code; if overlapping conformances were available I'd use them here, but I'm not going to ask for them just to be able to use them here.

Also, I'm skeptical these gains would be realized in practice: between the proposed "least specialized conformance wins" rule and Swift's existing dispatch rules in generic contexts, it seems like even if overlapping conformances *were* allowed and I *did* use them, I'd still wind up getting dispatched to the pessimal `SS` variant in many cases for which I'd have been hoping for one of the more-optimal versions.

So between the niche-ness of such uses -- and their being 5% or less of what I was hoping to do -- and my skepticism about how dispatch would pan out in practice, I can't get behind fighting for overlapping conformances at this time unless they'd be permanently banned by banning them now.

As already stated, I do think that their absence *will* reveal some *true* pain points, but I think it makes sense to adopt a "wait-and-see" approach here as some more-targeted solution could wind up being enough to address the majority of those future pain points.

These are my more-detailed thoughts after looking at what I was planning to do with conditional conformances once the became available. I realize it doesn't touch on every conceivable scenario and every conceivable use, but I want to reiterate that I did my review expecting to find a bunch of things that I could use as justifications for why Swift absolutely should have overlapping conditional conformances right now...but on actually looking at my plans, I couldn't find anything for which I actually felt that way.

> 	- Doug

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20161002/07f0215d/attachment.html>

More information about the swift-evolution mailing list