[pattern-switch] Exhaustiveness
Brian Goetz
brian.goetz at oracle.com
Fri Aug 14 17:20:26 UTC 2020
>
> - Exhaustiveness and null. (Tagir) For sealed domains (enums and
> sealed types), we kind of cheated with expression switches because we
> could count on the switch filtering out the null. But Tagir raises an
> excellent point, which is that we do not yet have a sound definition
> of exhaustiveness that scales to nested patterns (do Box(Rect) and
> Box(Circle) cover Box(Shape)?) This is an interaction between sealed
> types and patterns that needs to be ironed out. (Thanks Tagir!)
[ Breaking this out from Tagir's more comprehensive reply ]
> It's unclear for me how exhaustiveness on nested patterns plays with
> null. case Box(Circle c) and case Box(Rect r) don't cover case
> Box(null) which is a valid possibility for Box<Shape> type.
It’s not even clear how exhaustiveness plays with null even without
nesting, so let's start there.
Consider this switch:
switch (trafficLight) {
case GREEN, YELLOW: driveReallyFast();
case RED: sigh();
}
Is it exhaustive? Well, we want to say yes. And with the existing
null-hostility of switch, it is. But even without that, we’d like to
say yes, because a null enum value is almost always an error, and making
users deal with cases that don’t happen in reality is kind of rude.
For a domain sealed to a set of alternatives (enums or sealed classes),
let’s say that a set of patterns is _weakly exhaustive_ if it covers all
the alternatives but not null, and _strongly exhaustive_ if it also
covers null. When we did switch expressions, we said that weakly
exhaustive coverings didn’t need a default in a switch expression. I
think we’re primed to say the same thing for sealed classes. But, this
“weak is good enough” leans on the fact that the existing hostility of
switch will cover what we miss. We get no such cover in nested cases.
I think it’s worth examining further why we are willing to accept the
weak coverage with enums. Is it really that we’re willing to assume
that enums just should never be null? If we had type cardinalities in
the language, would we treat `enum X` as declaring a cardinality-1 type
called X? I think we might. OK, what about sealed classes? Would the
same thing carry over? Not so sure there. And this is a problem,
because we ultimately want:
case Optional.of(var x):
case Optional.empty():
to be exhaustive on Optional<T>, and said exhaustiveness will likely
lean on some sort of sealing.
This is related to Guy's observation that totality is a "subtree all the
way down" property. Consider:
sealed class Container<T> permits Box, Bag { }
sealed class Shape permits Rect, Circle { }
Ignoring null, Box+Bag should be exhaustive on container, and
Rect+Circle should be exhaustive on shape. So if we are switching over
a Container<Shape>, then what of:
case Box(Rect r):
case Box(Circle c):
case Bag(Rect r):
case Bag(Circle c):
We have some "nullity holes" in three places: Box(null), Bag(null), and
null itself. Is this set of cases exhaustive on Bags, Boxes, or
Containers?
I think users would like to be able to write the above four cases and
treat it as exhaustive; having to explicitly provide Box(null) / Box b,
Bag(null) / Bag b, or a catch-all to accept null+Box(null)+Bag(null)
would all be deemed unpleasant ceremony.
Hmm...
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20200814/f75f80cb/attachment-0001.htm>
More information about the amber-spec-experts
mailing list