[swift-evolution] [swift-evolution-announce] [Review] SE-0089: Replace protocol<P1, P2> syntax with Any<P1, P2>

Austin Zheng austinzheng at gmail.com
Thu Jun 9 12:24:03 CDT 2016

On Thu, Jun 9, 2016 at 9:42 AM, Dave Abrahams via swift-evolution <
swift-evolution at swift.org> wrote:

> on Thu Jun 09 2016, Matthew Johnson <matthew-AT-anandabits.com> wrote:

> >>
> >>> Introducing a language (not library) mechanism that exposes members on
> >>> generalized existentials in a way that relies on runtime traps for
> >>> type safety feels to me like a pretty dramatic turn agains the stated
> >>> priority of safety.  It will mean you must understand exactly what is
> >>> going on and be extremely careful to use generalized existentials
> >>> without causing crashes.  This will either make Swift code much more
> >>> crashy or will scare people away from using generalized existentials
> >>> (and maybe both).
> >>
> >> I don't accept either of those statements without seeing some analysis
> >> of the use-cases.  For example, I don't believe that AnyCollection et al
> >> are particularly crash-prone.  The likelihood that you'll use the wrong
> >> index type with a collection is very, very low.  I'm less certain of
> >> what happens with Self requirements in real cases.
> >
> > But again, I believe this is an exceptional case as the precondition
> > is explicitly stated in the semantics of the protocol.
> IIUC, it has been cited by Doug as the exemplar of the
> predominantly-requested case by a 10:1 ratio!

At the risk of sounding waffly, I see both sides of the argument.

Java is typesafe in the formal sense (can't perform an operation on a type
that doesn't actually support it); almost certainly moreso than Swift
(which lets you do wonderful things like reinterpret casting, C-style
direct memory manipulation...).

However, in many cases Swift provides better compile-time assurances than
Java. In Java, any reference might actually be a null reference waiting to
cause a NullPointerException unless you explicitly check it in code (and
what if you forget to check it somewhere?). In Swift, (aside from IUOs and
a few other features) if your program compiles successfully the type
checker has proven that your software will never try calling a method upon
nil. And if you do use IUOs the postfix exclamation marks clearly mark
spots in your code that might cause trouble.

In the same way I don't think trapping at runtime if an existential
satisfying a generic type parameter causes a type error is the best
solution to this problem. What we are doing is allowing a type that makes a
weak guarantee to stand in for a type that makes a strong guarantee, and
catching mismatches between those guarantees if/when they pop up. There's
no easy way for someone perusing the code to tell that such a mismatch
might occur, equivalent to IUO's postfix !.

What I prefer is to explicitly "promote" the type with the weak guarantee
to a type with the stronger guarantee using the existential opening
mechanism that we've been throwing around. That way, it's very clear what
is going on: if the existentials in question can't serve as generic
parameters to the function you want to pass them to, you can fatalError()
or do something else. Or you can try force-opening with `as!` and mark a
potential trouble spot in the same way an IUO does. Once it succeeds you
know you have the strong guarantees that will cause your generic member to
alway work the way it should.

With the proposed mechanism, I expect that Any<Collection where .Element ==
T> should be less prone to runtime trapping than AnyCollection<T>, because
we can reify the concept that each instance of the existential has a
concrete Index type associated with it, rather than relying upon a
type-erased AnyIndex shared among all AnyCollections. (That being said, I
don't know if AnyCollection is particularly problematic in practical use. I
also don't know if other type-erased wrappers users might define might be
more or less prone to this type of issue.)

> > IMO the burden of proof should be on the side that proposes a
> > mechanism to introduce traps, not the side that proposes avoiding
> > them.
> If you really want to make this about sides and burdens, the burden of
> proof always rests with the side proposing to extend the language.  We
> shouldn't be making changes without understanding how they will play out
> in real use-cases.
Yes, I really want to see real-world use cases.

> >>
> >> Neither of these look like they actually make *use* of the fact that
> >> there's type erasure involved (and therefore should probably be written
> >> as generics?).  The interesting cases with Any<Collection...>, for the
> >> purposes of this discussion, arise when you have multiple instances of
> >> the same existential type that wrap different concrete types.
> >
> > One use case I have found is to work around the lack of higher-kinder
> > types.
> Really, now: a use-case for feature A that is a workaround for the lack
> of feature B hardly justifies adding feature A!  We do want to add
> higher-kinded types eventually.

I just want to say, this made my day.

Most of the examples in the document Matthew linked were meant to be
didactic, not practical. I would love to swap them out for more useful

> >>
> >> Another problem I see: in this new world, what is the model for choosing
> >> whether to write a function as a protocol extension/generic, or as a
> >> regular function taking existential parameters?  Given that either of
> >> the above could have been written either way, we need to be able to
> >> answer that question.  When existentials don't conform to their
> >> protocols, it seems to me that the most general thing to do is use
> >> existentials whenever you can, and only resort to using generics when
> >> forced by the type system.  This does not seem like a particularly good
> >> programming model to me, but I might be convinced otherwise.
> >
> > That doesn’t seem like a particularly good programming model to me
> either.
> >
> > The rule of thumb I am operating with for protocols with Self or
> > associated type requirements is to prefer generics and use type
> > erasure / existentials when that isn’t possible.  For example, when
> > heterogeneity is required or when you can’t form the necessary type in
> > a protocol requirement (as in the preceding example).
> >
> > This heuristic has been working out pretty well for me thus far.
> I do worry a bit that people will choose the opposite heuristic.

If we are to introduce this feature, I expect that the community would go
through another "reference vs value types - which is right for you?" type
discussion. It's true you can model a lot of things with classes where
structs would be more appropriate, but this doesn't seem to be a big issue.

I would be very interested in affordances that can steer people towards
using generics when the dynamic nature of existentials isn't necessary.

> ______________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160609/3496a876/attachment.html>

More information about the swift-evolution mailing list