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.

Reply via email to