On Sunday, October 2, 2016, plx via swift-evolution < swift-evolution@swift.org> wrote:
On Sep 30, 2016, at 1:23 PM, Douglas Gregor <dgregor@apple.com > <javascript:_e(%7B%7D,'cvml','dgregor@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