Pattern Matching and null references
John Rose
john.r.rose at oracle.com
Fri Aug 4 02:51:07 UTC 2017
On Aug 2, 2017, at 7:26 PM, Tagir Valeev <amaembo at gmail.com> wrote:
>
> Indeed, I forgot about null today. Even if hierarchy of
> Node/AddNode/NegNode/MulNode/IntNode is sealed, null case was not covered
> on the slide.
Our intention is to support existing semantics for existing switches.
Those semantics are null-hostile. But, for new code, we intend to
allow null-friendly switches. There are a range of choices in how
to do this, but it boils down to some variation of "match a null to the
first case that allows a null, and throw NPE otherwise". This implies
that when you want a null-friendly switch and you want the null to
go to the default, you spell it with fallthrough: "case null: default:".
The advantage of null hostility is that you can code as if null were
always bugs (and sometimes they are). The advantage of allowing
users to write null-friendly switches is, well, it avoids dictating to
the user what their null policy should be. I don't expect to profit
from any conversation that includes "You can't use our new
language features if you are going to tolerate nulls in your code."
After we get comfortable with fixing up null like this, we will have
some experience for dealing with the question of how to deal with
expression-switches that lack a default case. Such switches
may be called "no-match hostile", since they must throw an
exception if there is no matching case. I think we need a new
exception type here, but it has very much the same flavor as
NPE: It diagnoses a logic error that a properly constructed
program should not encounter. Ideally there should be a static
completeness check that creates a warning that such exceptions
may arise. (Sort of like static nullability checks.) The no-match
exceptions can also arise due to separate compilation, so there
is no hard-and-fast rule for excluding them. (Like NPE, again.)
For replacing "instanceof and cast" by "match and bind", we expect
to reject nulls, but NOT throw NPE on them. After all, "instanceof"
quietly fails on null values. For replacing "extract boxed primitive
by cast" with a "match and bind", we have an option to reject a
null box either silently (with a match failure) or noisily (with NPE).
Although switches have a legacy NPE behavior which we must
rationalize, test-and-cast patterns do NOT have a legacy NPE
behavior, so it would be an error to introduce NPEs from some
notion of symmetry.
For cases where the user wants an NPE rather than a silent
match failure, an expression-switch is the right tool. This
applies to code which unboxes primitives:
int x = (int) mymap.get("foo"); // may NPE
becomes:
int x = switch (mymap.get("foo")) { case int x -> x; };
(BTW, the above wants to be a type test of Integer but looks suspiciously
like a type-restatement, which I have argued should pass 'null'. Hmm;
that's evidence against a proposal I gave earlier.)
But if you want to default a null to some sentinel like -1, you can
roll it into the switch:
int x = switch (mymap.get("foo")) { case int x -> x; case null -> -1; };
It might even be possible to attach null-friendliness to individual match
patterns. Given certain plausible interpretations of && and || operators
for match expressions we get this:
if (mymap.get("foo") matches (int x || null && -1 matches int x) {
… use x …
}
The idiom "null && 0 matches int x" requires some getting used to
but it falls out of the general rules for matches and bindings.
Probably it belongs in a utility deconstructor routine:
if (mymap.get("foo") matches maybeNull(int x, -1)) {
… use x …
}
Finally, one thing I think Andrey mentioned today is that patterns
should be allowed in *UNCONDITIONAL* contexts like let-variables
and lambda parameters. I agree with this; what is needed is a way
of saying "assert x matches P", as sugar for a no-match hostile
switch of one case.
A lambda might look like:
(case Pair(var x, var y)) -> Pair(y, x)
A let-binding might look like:
Pair v = …;
case Pair(var x, var y) = v; // bad bikeshed here
return Pair(y, x);
These would be sugar for no-match hostile switches:
(v) -> switch (v) matches { case Pair(var x, var y) -> Pair(y, x) }
Pair v = …;
switch (v) { case Pair(var x, var y):
// NOTE NEW INDENTATION LEVEL
return Pair(y, x);
default: throw new NoMatchException();
}
— John
More information about the amber-dev
mailing list