Re: [External] : Re: Primitive type patterns

2022-02-26 Thread John Rose

On 26 Feb 2022, at 8:49, Brian Goetz wrote:

…I think they mostly proceed from two places where we may continue 
to disagree:


 - You are generally much more inclined to say "if it might be null, 
disallow it / throw eagerly" than I am.  In general, I prefer to let 
the nulls flow until they hit a point where they can clearly flow no 
further, rather than introduce null gates into the middle of 
computations, because null gates are impediments to composition and 
refactoring.


Now added to the lexicon:  “null gates”.

Here’s a slogan to go with it:  “No new null gates.”  It’s 
logically the same as “Let the nulls flow [until they really 
can’t]”.


 - You are viewing pattern matching as the "new thing", and trying to 
limit it to the cases where you're sure that users who are unfamiliar 
with it (which is almost all of them) will have a good initial 
experience.  (This is sort of a semantic analogue of Stroustrup's 
rule.)  But I believe those limitations, in the long run, will lead 
to a more complex language and a worse long-term experience.  I want 
to optimize for where we are going, which is that there is one set of 
rules for patterns people can reason about, even if they are a little 
complicated-seeming at first, rather than an ever-growing bag of 
individually "simple" restrictions.


I buy this argument too:  A rationalizing retcon for the existing 
assignment conversion rules will reduce the overall cost of adding 
patterns.  There might be a transient cost to attaching pattern 
conversions at the deepest level of the language, compared to making 
patterns into pure sugar bolted on the side.  But in the end I’d 
rather learn something that *does* have deep connections to the 
language.

Re: [External] : Re: Primitive type patterns

2022-02-26 Thread Brian Goetz




 Relationship with assignment context


That's a huge leap, let's take a step back.

I see two questions that should be answered first.
1) do we really want pattern in case of assignment/declaration to 
support assignment conversions ?
2) do we want patterns used by the switch or instanceof to follow the 
exact same rules as patterns used in assignment/declaration ?


I agree we should take a step back, but let's take a step farther -- 
because I want to make an even bigger leap that you think :)


Stepping way far back  in the beginning ... Java had reference types 
with subtyping, and eight primitive types.  Which raises an immediate 
question: what types can be assigned to what?  Java chose a sensible 
guideline; assignment should be allowed if the value set on the left is 
"bigger" than that on the right.  This gives us String => Object, int => 
long, int => double, etc.  (At this point, note that we've gone beyond 
strict value set inclusion; an int is *not* a floating point number, but 
we chose (reasonably) to do the conversion because we can *embed* the 
ints in the value set of double.   Java was already appealing to the 
notion of embedding-projection pair even then, in assignment 
conversions; assignment from A to B is OK if we have an embedding of A 
into B.)


On the other hand, Java won't let you assign long => int, because it 
might be a lossy conversion.  To opt into the loss, you have to cast, 
which acknowledges that the conversion may be information-losing.  
Except!  If you can prove the conversion isn't information losing 
(because the thing on the right is a compile-time constant), then its 
OK, because we know its safe.  JLS Ch5 had its share of ad-hoc-seeming 
complexity, but mostly stayed in its corner until you called it, and the 
rules all seemed justifiable.


Then we added autoboxing.  And boxing is not problematic; int embeds 
into Integer.  So the conversion from int => Integer is fine. (It added 
more complexity to overload selection, brought in strict and loose 
conversion contexts, and we're still paying when methods like 
remove(int) merge with remove(T), but OK.)  But the other direction is 
problematic; there is one value of Integer that doesn't correspond to 
any value of int, which is our favorite value, null. The decision made 
at the time was to allow the conversion from Integer => int, and throw 
on null.


This was again a justifiable choice, and comes from the fact that the 
mapping from Integer to int is a _projection_, not an embedding.  It was 
decided (reasonably, but we could have gone the other way too) that null 
was a "silly" enough value to justify not requiring a cast, and throwing 
if the silly value comes up.  We could have required a cast from Integer 
to int, as we do from long to int, and I can imagine the discussion 
about why that was not chosen.


Having set the stage, one can see all the concepts in pattern matching 
dancing on it, just with different names.


Whether we can assign T to U with or without a cast, is something we 
needed a static rule for.  So we took the set of type pairs (T, U) for 
which the pattern `T t` is strictly total on U, and said "these are the 
conversions allowed in assignment context" (with a special rule for when 
the target is an integer constant.)


When we got to autoboxing, we made a subjective call that `int x` should 
be "total enough" on `Integer` that we're willing to throw in the one 
place it's not.  That's exactly the concept of "P is exhaustive, but not 
total, on T" (i.e., there is a non-empty remainder.)  All of this has 
happened before.  All of this will happen again.


So the bigger leap I've got in mind is: what would James et al have 
done, had they had pattern matching from day one?  I believe that:


 - T t = u would be allowed if `T t` is exhaustive on the static type of u;
 - If there is remainder, assignment can throw (preserving the 
invariant that if the assignment completes normally, something was 
assigned).


So it's not that I want to align assignment with pattern matching 
because we've got a syntactic construct on the whiteboard that operates 
by pattern matching but happens to looks like assignment; it's because 
assignment *is* a constrained case of pattern matching.  We've found the 
missing primitive, and I want to put it under the edifice.  If we define 
pattern matching correctly, we could rewrite JLS 5.2 entirely in terms 
of pattern matching (whether we want to actually rewrite it or not, 
that's a separate story.)


The great thing about pattern matching as a generalization of assignment 
is that it takes pressure off the one-size-fits-all ruleset.  You can write:


    int x = anInteger

but it might throw NPE.  In many cases, users are fine with that. But by 
interpreting it as a pattern, when we get into more flexible constructs, 
we don't *have* to throw eagerly.  If the user said:


    if (anInteger instanceof int x) { ... }

then we match the pattern 

Re: Primitive type patterns

2022-02-26 Thread Remi Forax
> From: "Brian Goetz" 
> To: "amber-spec-experts" 
> Sent: Friday, February 25, 2022 10:45:44 PM
> Subject: Primitive type patterns

> As a consequence of doing record patterns, we also grapple with primitive type
> patterns. Until now, we've only supported reference type patterns, which are
> simple:

> - A reference type pattern `T t` is applicable to a match target of type M if 
> M
> can be cast to T without an unchecked warning.

> - A reference type pattern `T t` covers a match type M iff M <: T

> - A reference type pattern `T t` matches a value m of type M if M <: T || m
> instanceof T

> Two of these three characterizations are static computations (applicability 
> and
> coverage); the third is a runtime test (matching). For each kind of pattern, 
> we
> have to define all three of these.

>  Primitive type patterns in records

> Record patterns necessitate the ability to write type patterns for any type 
> that
> can be a record component. If we have:

> record IntBox(int i) { }

> then we want to be able to write:

> case IntBox(int i):

> which means we need to be able to express type patterns for primitive types.

>  Relationship with assignment context

> There is another constraint on primitive type patterns: the let/bind statement
> coming down the road. Because a type pattern looks (not accidentally) like a
> local variable declaration, a let/bind we will want to align the semantics of
> "local variable declaration with initializer" and "let/bind with total type
> pattern". Concretely:

> let String s = "foo";

> is a pattern match against the (total) pattern `String s`, which introduces 
> `s`
> into the remainder of the block. Since let/bind is a generalization of local
> variable declaration with initialization, let/bind should align with locals
> where the two can express the same thing. This means that the set of
> conversions allowed in assignment context (JLS 5.2) should also be supported 
> by
> type patterns.

> Of the conversions supported by 5.2, the only one that applies when both the
> initializer and local variable are of reference type is "widening reference",
> which the above match semantics (`T t` matches `m` when `M <: T`) support. So
> we need to fill in the other three boxes of the 2x2 matrix of { ref, primitive
> } x { ref, primitive }.
That's a huge leap, let's take a step back. 

I see two questions that should be answered first. 
1) do we really want pattern in case of assignment/declaration to support 
assignment conversions ? 
2) do we want patterns used by the switch or instanceof to follow the exact 
same rules as patterns used in assignment/declaration ? 

For 1, given that we are using pattern to do destructured assignment, we may 
want to simplify the assignment rules to keep things simple avoid users 
shooting themselves in the foot with implicit unboxing. 
With an example, 
record Box(T value) {} 
Box box = ... 
Box<>(int result) = box; // assignment of result may throw a NPE 

I don't think we have to support that implicit unboxing given that we have a 
way to ask for an unboxing explicitly (once java.lang.Integer have a 
de-constructor) 

Box<>(Integer(int result)) = box; 

I think we should not jump with the shark too soon here and ask ourselves if we 
really want assignment conversions in case of destructured assignment. 

2) we already know that depending on the context (inside a switch, inside a 
instanceof, inside an assignment) the rules for pattern are not exactly the 
same. 
So we may consider that in the assignment context, assignment conversions apply 
while for a matching context, simpler rules apply. 
Given that the syntax for switch reuse '->', i believe we should use the 
overriding rules (the one we use for lambdas) instead of the assignment rules 
(the one we use for method reference). 
And yes, i know that the '->' of switch is not the same as the '->' of lambda, 
but i don't think we should bother users to intuitively think that the same 
rules apply. 

Then the model you propose is too clever for me, the fact that 
instanceof Point(double x, double y) 
has a different meaning depending if Point is declared like 
record Point(double x, double y) { } 
or like this 
record Point(Double x, Double y) { } 
is too much. 

The semantics of Java around null is already a giant landmine field, we should 
restraint ourselves to add more null-funny behaviors. 

regards, 
Rémi