Updated pattern match documents
Brian Goetz
brian.goetz at oracle.com
Tue Sep 11 10:41:17 UTC 2018
> I think, this part requires clarification. Some interesting cases:
>
> switch(boolValue) {case true:case false:case _:break;} -- is case _ a
> compilation error? What if case _ is changed to default?
> switch(boxedBoolValue) {case true:case false:case null:case _:break;}
> -- the same question
So, boolean is slightly easier than enum, because it's a lot less likely
that new boolean values will appear via separate compilation than with
enums. Though, it sometimes happens:
https://thedailywtf.com/articles/What_Is_Truth_0x3f_
So let's start with enums. Today, users often have to code a `default`
branch even when all enums are covered. This is annoying, and we'd like
to give users some relief from having to include silly code, but the
motivation goes deeper than that. If you have a switch expression over
an enum without a default, the compiler will tell you when you've left
out a case. But if you have a default, the compiler will happily let
you cover N-1 of the cases. So not only is requiring a silly default
annoying to the user, it takes away the compiler's ability to type
check. So clearly we want users to be able to enter exhaustive switches
(over booleans, enums, sealed types, and maybe even byte) without default.
That said, we probably still want to allow a default even when the
switch is exhaustive, because perhaps the user wants to customize the
handling of such problems with a more scrutable error than simply
getting an ICCE. So, even though `default` / case _ might be dead at
compile time, it might not be dead at runtime. (For boolean and byte,
OK, it's dead. But that's a special case.)
That's a different case (heh) than:
case Integer i -> 0;
case integer j -> 1;
The second arm is going to be dead no matter what. The examples of
dominance that I gave were primarily type-based, so they fall in the
latter category. We should reject the second arm.
So, I think the dominance story holds up, but we do need to adjust it to
handle cases that are exhaustive at compile time but are not guaranteed
to be so at run time (which is primarily restricted to enums and sealed
types.)
(Also note that exhaustiveness checking can easily become a lifetime
activity. The easy cases are easy, but there's a long tail of
whack-a-mole that probably ends in the halting problem.)
> assuming enum Direction {UP, DOWN} and Box(Direction)
> switch(box) {case Box(UP):case Box(DOWN):case Box b:break;} -- I
> assume that case Box b is allowed here as it can match Box(null). Even
> if Box(Direction) constructor requires that Direction is not null,
> compiler cannot know this. Or can?
Currently can not. But even if it could, we're back in enum territory,
and its possible that a new direction, LEFT, shows up at runtime. So a
`Box` or `Box(var x)` or `_` case are acceptable here. But I think this
derives from the fact that enums are inherently more fungible than
booleans?
So, you're really asking two questions:
- Under what conditions do we require a catch-all case?
- Under what conditions do we allow a catch-all case, even if the
switch seems exhaustive?
For the first, we require a catch-all if (a) the switch is an expression
switch and (b) we cannot prove that the cases are exhaustive _relative
to compile-time type knowledge_. So if you switch on enums:
case (trafficLightColor) {
case RED:
case YELLOW:
case GREEN:
}
you're good, even though a new color could come along later. The
compiler will insert a catch-all case here, throwing ICCE. And there's
no null possible, since none of the cases are nullable, so the switch
will throw on null.
Raising it up a level:
case (box) { // assume Box<TrafficLightColor>
case Box(RED): ...
case Box(YELLOW): ...
case Box(GREEN): ...
}
we can detect at compile time that we haven't handled Box(null), though
it seems a bit mean to require a default here. So we may want to treat
null specially here -- let's think about that.
For the latter question, I think when we are reasoning about
exhaustiveness in a brittle way (enums and sealed classes), we want to
allow a catch-all case even if it seems dead at compile time. But if
we're reasoning about exhaustiveness through types, then a `default` or
`_` case might be truly dead, and we should reject it.
> Also what about combinations of several fields? Assuming
> TwoFlags(boolean, boolean):
> switch(twoFlags) {case TwoFlags(true, _):case TwoFlags(_, true):case
> TwoFlags(false, false):case TwoFlags t:break;} -- here all
> combinations are covered by first three patterns, so TwoFlags t cannot
> match anything. Will it be reported? What if we have ten enum fields?
> Will compiler track all covered value combinations and issue an error
> if it's statically known that some pattern is unreachable? I'm not
> sure about computational complexity of such tracking in common case.
This is starting down that slippery slope of "lifetime activity" I was
referring to :)
More information about the amber-spec-experts
mailing list