> On Sep 30, 2016, at 1:23 PM, Douglas Gregor <dgre...@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 
    where 
    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
    where 
    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

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to