Primitives in instanceof and patterns

Brian Goetz brian.goetz at oracle.com
Sun Sep 11 14:48:04 UTC 2022



>>
>> I think you're falling into the trap of examining each conversion and
>> asking "would I want a pattern to do this."
> Given that only primitive widening casts are safe, allowing only primitive widening is another way to answer to the question what a primitive type pattern is.
> You are proposing a semantics using range checks, that's the problem.

So, substitute "reference" for "primitive" in this argument, and you 
will see how silly it is: "since only reference widening is safe, 
allowing only reference widening would be 'another answer to what a 
reference type pattern is.'"  But that would also be a useless 
semantic.  You're caught up on "range checks", but that's not the 
important thing here.  Casting is the important thing.

> As an example, instanceof rules and the rules about overriding methods 
> are intimately linked, asking if a method override another is 
> equivalent to asking if their function types are a subtypes.
> if int instanceof double is allowed, then B::m should override A::m
>    class A {
>      int m() { ... }
>    }
>    class B extends A {
>      @Override
>      double m() { ... }
>    }
>
> This is what i meant by changing other rules.

Another cute argument, but no.  Covariant overriding is linked to 
*subtyping*.  Instanceof *happens to coincide* with subtyping right now 
(given its ad-hoc restrictions), but the causality goes the other way.  
(Casting also appeals to subtyping, through reference widening 
conversions.)  But this argument is like starting with "all men are 
moral" and "Socrates is a man" and concluding "All men are Socrates."

We can talk about whether it would be wise to align the definition of 
covariant overrides with conversions other than reference widening (and 
will likely come up again in Valhalla anyway), but this is by no means a 
forced move, and not tied to generalizing the semantics of instanceof.

> I found a way to explain clearly why a reference type pattern and a 
> primitive type pattern are different.
>
> Let suppose that the code compiles (to avoid the issues of the 
> separate compilation),
> unlike a reference type pattern, the code executed for a primitive 
> type pattern is a function of *both* the declared type and the pattern 
> type.

So (a) untrue -- what code we execute for a reference type pattern does 
depend on the static types -- we may or may not generate an `instanceof` 
instruction, depending on whether the pattern is unconditional.  (The 
same is true for a cast; some casts are no-ops and generate no code.)  
And (b), so what?  We're asking "would it be safe to cast x to T".  
Depending on the types X and T, we will have different code for the 
casting, so why is it unreasonable to have different code for asking 
whether it is castable?

>
> By example, if i have a code like this, i've no idea what code is 
> executed for case Foo(int i) without having to go to the declaration 
> of Foo which is usually not collocated with the switch itself.
>
>    Foo foo = ...
>    switch (foo) {
>       case Foo(int i) -> {}
>       case Foo(double d) -> {}
>     }

Sigh, this argument again?  We've been through this extensively the 
first time around, with reference types, where you "had no idea what 
this code means" without looking at the declaration of the pattern. 
(Then, it was partiality and totality.)  I get that you didn't like that 
total and partial patterns don't look syntactically different, and that 
ship has sailed.  But this is the same argument warmed over.

"What code will be executed" is irrelevant; what is relevant is the 
semantics.  Assuming a single deconstruction pattern for Foo, the first 
case asks "can the Foo's component be cast safely to int, and if so, 
please cast it for me".  It doesn't matter what code we use to answer 
that question or do the cast -- could be a narrowing, could be an 
unboxing, whatever.

You see the same thing today without patterns:

     var x = foo.getFoo();
     int i = (int) x;

x could be a long, an int, an Integer, etc, but you don't know unless 
you look at the definition of getFoo().  And you have "no idea what code 
will be executed."  Sure, but so what?  You asked for a cast to int.  
The language validated that x is castable to int, and does what needs to 
be done, which might be nothing, or a widening, or a narrowing with 
truncation, or an unboxing, or some combination.

(When we get to overloading deconstruction patterns, we'll have all the 
same issues as we have with overloading methods today -- it is not 
obvious looking only at the call site, which overload is called, and 
therefore which conversions are applied to arguments or returns.)

As a reminder, here's what a nested pattern means:

     x matches P(Q) === x matches P(var q) && q matches Q

Understanding what is going to happen involves understanding the type of 
`q`.  I get that you didn't like that choice, and that's your right, but 
it's not OK to keep bringing it up as if its a new thing.


I think I actually understand your concern here, which has nothing to do 
with the dozen or so bogus examples and explanations you've tossed out 
so far.  It is that cast conversion is complicated, and you would like 
pattern matching to be "simple", and so pulling in the muck of cast 
conversion into pattern matching feels to you like an unforced error.  
Right?  (And if so, perhaps you could have just said that, instead of 
throwing random arguments at the wall?)

I also would like to hear from more people in this discussion, and I 
don't think the style of discourse we've fallen into (again) is 
conducive to that.



More information about the amber-spec-observers mailing list