Relaxed assignment conversions for sealed types
Brian Goetz
brian.goetz at oracle.com
Tue Nov 24 15:24:22 UTC 2020
> To throw in the mix - how is some kind of pattern match assignment (we
> referred to as a "let expression" in some of the earlier docs [1])
> would change the picture here? In other words, maybe it's overloading
> `=` which is at odds here, and we need to make it more explicit that
> this is more akin to an extraction/match?
>
So, I think "let statements" beat "total casts" to the overload here :)
<background>
Originally, we imagined a "let" statement like:
let P = e [ else ... ]
where P is an optimistically total pattern.
<digression>
We pulled on the "what can you do in the else, besides throw" string for
a while, but didn't love where it ended up. The obvious thing you might
want to do besides throw or return, is to assign to the pattern
variables and keep going:
let Point(var x, var y)
else { x = 0; y = 0; }
This was messy when pattern variables were implicitly final, but now
that we backed away from that siren song (whew), these become simple
assignments to DU locals.
</digression>
But over time, the `let` started to rankle, not only as noise, but
because it limited the usefulness. The observation is that (again, now
that we backed away from the siren song of pattern variables being final):
Point p = <expression>
could either be the declaration of a local of type Point, _or_ a pattern
match to the total pattern `Point p`. In other words, what we
historically thought of as local variable declaration, could in fact be
generalized to total pattern matching. Not only is this aesthetically
pleasing, but it means we can extend this idiom in two dimensions: other
patterns, and other places.
The "other patterns" is straightforward enough:
Point(var x, var y) = p
The only challenge here is what to do with the remainder, but we've done
enough work on characterizing the remainder to be comfortable enough
with throwing in those cases (usually NPE.)
The "other places" takes slightly longer to get used to, such as method
declarations:
void foo(Point(var x, var y) p, int z) { ... }
which is equivalent to:
void foo(Point p, int z) { Point(var x, var y) = p; ... }
and a similar possibility with lambdas:
(Point(var x, var y) p) -> ...
All of this is to say that (a) the "let" statement structure was getting
in the way, and (b) there's a generalization of local declaration
struggling to get out here, and (c) pattern totality is the key to it.
</background>
Given all this, it would be justifiable to not require the cast at all!
The pattern `BarImpl b` is total on `Bar` (assuming Bar is abstract and
sealed only to BarImpl), by virtue of the rules for totality that are
outlined in [1]. So by this interpretation of "local declaration
generalizes to total pattern matching", we could just say:
BarImpl b = bar;
and be done, since the expression `bar` is of type Bar and the pattern
`BarImpl b` is optimistically total on Bar. (If the sealing changes
between compile and run time, this could throw CCE.)
But some people might find that uncomfortable, which is how we got here;
they want the optimism to be explicit. (Backing off to "let" seems a
strict loser compared to "total cast", since total cast has much more
leverage for the same incremental syntactic footprint (and requiring let
where things are more obvious will be seen as noise, and the
statement-ness of `let` gets in the way of using it in more places.))
If this is too aggressive, we would want to limit the `P = e` form to
strict totality, or more likely, totality with only { null } remainder
(as this would permit using deconstruction patterns on the LHS.)
The blind cast is surely allowed, but its not something we want to
encourage -- and as both you and I have found (perhaps the only people
who have yet written significant libraries with sealing?), it is going
to be almost irresistible to avoid embedding the blind-cast assumptions
in your code.
So this is how I got to a "blind cast with assertion", not unlike an
@Override assertion -- cast, but raise a compile-time error if the cast
is not optimistically total. Instanceof and switch learned about
totality; perhaps casting should too.
[1]
https://github.com/openjdk/amber-docs/blob/master/site/design-notes/type-patterns-in-switch.md
More information about the amber-spec-experts
mailing list