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