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