Primitive type patterns
Brian Goetz
brian.goetz at oracle.com
Mon Feb 28 16:50:25 UTC 2022
Let me put the central question here in the spotlight.
> #### Boxing and unboxing
>
> Suppose our match target is a box type, such as:
>
> record IntegerBox(Integer i) { }
>
> Clearly we can match it with:
>
> case IntegerBox(Integer i):
We could stop here, and say the pattern `int i` is not applicable to
`Integer`, which means that `IntegerBox(int i)` is not applicable to
IntegerBox. This is a legitimate choice (though, I think it would be a
bad one, one that we would surely revisit sooner rather than later.)
Users might well not understand why they could not say
case IntegerBox(int i)
because (a) you can unbox in other contexts and (b) this has a very
clear meaning, and one analogous to `case ObjectBox(String s)` -- if the
box contents fits in a String, then match, otherwise don't. They might
get even more balky if you could say
int x = anInteger
but not
let int x = anInteger
And, if we went with both Remi's preferences -- the minimalist matching
proposal and dropping "let" in the let/bind syntax (which we will NOT
discuss further here) then the following would be ambiguous:
int x = anInteger // is this an assignment, in which case we
unbox, or a match, in which case we don't?
and we would have to "arbitrarily" pick the legacy interpretation.
But all of this "balking" is symptom, not disease.
The reality is that pattern matching is more primitive than unboxing
conversions, and if we lean into that, things get simpler. An unboxing
conversion may seem like one thing, but is actually two: try to match
against a partial pattern, and if there is no match, fail. In other
words, unboxing is:
int unbox(Integer n) ->
switch(n) {
case int i -> i;
case null -> throw new NPE();
}
The unboxing we have jams together the pattern match and the
throw-on-fail, because it has no choice; unboxing wants to be total, and
there's no place to specify what to try if the pattern match fails. But
pattern matching is inherently conditional, allowing us to build
different constructs on it which handle failures differently.
So *of course* there's an obvious definition of how `int x` matches
against Integer, and its not a question of whether we "define" it that
way, its a question of whether we expose the obvious meaning, or
suppress it. I think the arguments in favor of suppression are pretty weak.
There's a similar argument when it comes to narrowing from (say) long to
int. There's a very natural interpretation of matching `int x` to a
long: does the long value fit in an int. In assignment context, we do
the best we currently can -- we allow the narrowing only if we can
statically prove it will match, which mean if the match target is a
constant. But again, conversions in assignment context are not the
primitive. If we take the obvious definition of matching `int x`
against long, then the current rules fall out naturally, and we can ask
sensible questions like
if (aLong instanceof byte) { ... }
else if (aLong instanceof short) { ... }
by *asking the primitive directly*, rather than appealing to some proxy
(like manually unrolling the range check.)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20220228/935fb426/attachment-0001.htm>
More information about the amber-spec-experts
mailing list