Primitive type patterns - an alternative approach (JEP 507)
Brian Goetz
brian.goetz at oracle.com
Thu Oct 16 16:22:38 UTC 2025
>> Zooming out, design almost always involves "lump vs split" choices; do we highlight the specific differences between cases, or their commonality?
> Another way to express this distinction is "what level of magic is acceptable?"
Heh, that's a pretty loaded way to express it.
Having semantics depend on static types is not "magic", whether or not
the types are repeated at every line of code they are used. When we say
int x = (int) anObject
vs
int x = (int) aLong
the two casts to int have different semantics _based on the type of what
is being cast_; one will attempt to cast the Object to Integer and then
unbox (possibly CCEing), and the other will attempt to narrow the long
to an int (possibly losing data). And yet, they both appear to be "the
same thing" -- casting a value to int.
Your arguments about JEP 507 could equally well be applied to the
semantic difference between pair of statements above. So why is this
not a disaster, or even "magic"? Because static types are a core part
of the Java language! Appealing to them, even if they are explicitly
denoted only somewhere else, is not magic.
It would be a useful thought experiment to ask yourself why the above
two examples don't offend you to the point of proposing new syntax.
Because all the implicit, type-driven variation in semantics that is
present in `anObject instanceof int x` is equally present in `int x =
anObject`. (In fact, it should be, because they are _the same thing_.)
So no, I can't agree that this is about "magic" at all. Let's use the
right word: "implicit". Your core argument is that "too much is left
implicit here, and therefore no one will be able to understand what is
going on." These sort of "it's OK now, but if we do one more thing it
will get out of hand" arguments remind me of previous arguments around
previous features that involved new implicit behaviors driven by static
types, which were predicted by their detractors to be raging disasters,
and which turned to be ... fine.
Example 1: autoboxing
Prior to Java 5, there was no implicit or explicit conversion between
`int` and `Integer` (not even casting); boxing and unboxing were done
manually through `new Integer(n)`, `Integer.valueOf(n)`, and
`Integer::intValue`. In Java 5, we added boxing and unboxing
conversions to the list of conversions, and also, somewhat more
controversially, supported "implicit" boxing and unboxing conversions
(more precisely, allowing them in assignment/method context) as well as
"explicit" boxing and unboxing conversions (casting).
Of course, some people cheered ("yay, less ceremony") but others gasped
in horror. An assignment that ... can throw? What black magic is
this? This will make programs less reliable! And the usual
bargaining: "why does this have to be implicit, what's wrong with
requiring an explicit cast?" 20 years later, this may seem comical or
hard to believe, but there was plenty of controversy over this in its day.
While the residue of complexity this left in the spec was nontrivial
(added to the complexity of both conversions and overload selection,
each nontrivial areas of the language), overall this was a win for Java
programmers. The static type system was still in charge, clearly
defining the semantics of our programs, but the explicit ceremony of "go
from int to Integer" receded into the background. The world didn't end;
Java programs didn't become wildly less reliable. And if we asked
people today if they wanted to go back, the answer would surely be a
resounding "hell, no."
Example 2: local variable type inference (`var`)
The arguments on both sides of this were more dramatic; its supporters
went on about "drowning in ceremony", while its detractors cried "too
much! too much!", warning that Java codebases would collapse into
unreadability due to bad programmers being unable to resist the
temptation of implicitness. Many strawman examples were offered as
evidence of how unreadable Java code would become. (To be fair, these
people were legitimately afraid for how such a feature would be used,
and how this would affect their experience of programming in Java,
fearing it would be overused or abused, and that we wouldn't be able to
reclose Pandora's box. (But some were just misguided mudslinging, of
course; the silliest of them was "you're turning Java into Javascript",
when in fact type inference is based entirely on ... static types.
Unfortunately there is no qualification exam for making strident
arguments.))
Fortunately, some clearer arguments eventually emerged from this chaos.
People pointed out that for many local variables, the _variable name_
carried far more information than the variable type, and that the
requirement to manifestly type all variables led to distortions in how
people coded (such as leading to more complicated and deeply nested
expressions, that could have benefited by pulling out subexpressions
into named variables).
In the end, it was mostly a nothingburger. Developers learned to use
`var` mostly responsibly, and there was no collapse in maintainability
or readability of Java code. The fears were unfounded.
One of the things that happens when people react to new features that
are not immediately addressing a pain point that they happen to be
personally in, is to focus on all the things that might go wrong. This
is natural and usually healthy, but one of the problems with this
tendency is that in this situation, where the motivation of the feature
doesn't speak directly to us, we often don't have a realistic idea of
how and when and how often it will come up in real code. In the absence
of a concrete "yes, I can see 100 places I would have used this
yesterday", we replace those with speculative, often distorted examples,
and react to a fear of the unrealistic future they imply.
Yes, it is easy to imagine cases where something confusing could arise
out of "so much implicitness" (though really, its not so much, its just
new flavors of the same old stuff.) But I will note that almost all of
the example offered involve floating point, which mainstream Java
developers _rarely use_. Which casts some doubt on whether these
examples of "look how confusing this is" are realistic.
(This might seem like a a topic change, but it is actually closer to the
real point.) At this point you might be tempted to argue "but then why
don't we 'just' exclude floating point from this feature?" And the
reason is: that would go against the _whole point_ of this feature.
This JEP is about _regularization_. Right now, there are all sorts of
random and gratuitous restrictions about what types can be used where;
we can only use reference types in instanceof, we can't switch on float,
constant case switches are not really patterns yet, we can't use `null`
in nested pattern context, etc etc. Each of these restrictions or
limitations may have been individually justifiable at the time, but in
the aggregate, they are a pile of pure accidental complexity, make the
language harder to use and learn, create unexpected interactions and
gaps, and make it much much harder to evolve the language in the ways
that Valhalla aims to, allowing the set of numeric types that can "work
like primitives" to be expanded. We can get to a better place, but we
can't bring all our accidental complexity with us.
When confronted with a new feature, especially one that is not speaking
directly to pain points one is directly experiencing, the temptation is
to respond with a highly localized focus, one which focuses on taking
the claimed goals of this feature and trying to make it "safer" or
"simpler" (which usually also means "smaller".) But such localized
responses often have two big risks: they risk missing the point of the
feature (which is easy if it is already not speaking directly to you),
and they risk adding new complexity elsewhere in the language in aid of
"fixing" what seems "too much" about the feature in front of you.
This feature is about creating level ground for future work to build on
-- constant patterns, numeric conversions between `Float16` and
`double`, etc. But to make these features possible, we first have to
undo the accidental complexity of past hyperlocal feature design so that
there can be a level ground that these features can be built on; the
ad-hoc restrictions have to go. This JEP may appear to create
complicated new situations (but really, just less familiar ones), but it
actually makes instanceof and switch _simpler_ -- both by by removing
restrictions and by defining everything in terms of a small number of
more fundamental concepts, rather than a larger pile of ad-hoc rules and
restrictions. Its hard to see that at first, so you have to give it
time to sink in.
*If* it turns out, when we get to that future, that things are still too
implicit for Java developers to handle, we still have the opportunity
_then_ to offer new syntactic options for finer control over conversions
and partiality. But I'm not compelled by the idea of going there
preemptively (and I honestly don't think it is actually going to be a
problem.)
> Circling back to "what level of magic is acceptable?". The trouble
> here is that partial type patterns and unconditional type patterns
> already share the same syntax, and that is bad enough. To add in type
> conversions is just way too far. This isn't lumping, it is magic.
>
> Trying to read and decipher code with merged type checks and type
> conversions in patterns simply isn't possible without an excessive
> amount of external context, which is potentially very difficult to do
> in PRs for example.
>
> All my proposal really argues is that alternative syntaxes are
> available that make the code readable again. With ~ the visible syntax
> question becomes "if I can convert to an int ....". Other options are
> available.
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20251016/727daf01/attachment-0001.htm>
More information about the amber-dev
mailing list