Let's say I start with this:
switch (lunch) {
case Box(Soup s) {
if (s == null) {
System.err.println("Box of null");
} else {
System.err.println("Box of soup");
}
}
case Bag(Soup s): {
if (s == null) {
System.err.println("Bag of null");
} else {
System.err.println("Bag of soup");
}
}
}
I assume that you are saying Box permits Soup only. But your
assumptions about "where do the nulls go" here are not right. Box(Soup)
does not match Box(null); the set of patterns { Box(Soup) } is total on
Box(Lunch) _with remainder Box(null)_. So the null paths in this
example are dead. (Also missing break statements.) So rewriting, this
switch is really equivalent to:
switch (lunch) {
case Box(Soup s):
System.err.println("Box of soup");
break;
case Bag(Soup s):
System.err.println("Bag of soup");
break;
/* implicit */
case Box(null), Bag(null): throw new NPE();
}
and the switch is total on Container(Lunch) under the Lunch=Soup,
Container=Box|Bag assumptions.
Then Sandwich is added to the hierarchy. The switch no longer
compiles, I have to make it total. The following, which is, I believe,
the first thing that will come to mind:
switch (lunch) {
case Box(Soup s) { /* same as before */ }
case Box(Sandwich s) { ... }
case Bag(Soup s): { /* same as before */ }
case Bag(Sandwich s) { ... }
}
With the correct version of the first switch, your first idea for fixing
it to accomodate sandwiches is correct! The switch is now total on
Container<Lunch>, the old cases handle the same thing they did, the new
cases handle the new cases, and the implicit cases (Box(null) etc)
handle the same thing they did.
But if it is considered exhaustive, then this will compile, but the
null handling logic will be in the wrong place, and will be
essentially dead code (which the user might be unaware of).
No, because it was dead in the first place too. Box(null) and Bag(null)
were always treated as remainder.