Feedback: Using pattern context to make type patterns more consistent and manage nulls
Brian Goetz
brian.goetz at oracle.com
Sun Jan 24 16:56:14 UTC 2021
> When nulls were last discussed I expressed reservations about how they
> were intended to flow through switch/patterns. It looks like the
> latest approach is an improvement.
I agree, though I want to point out that it is a very small change from the previous iteration — it is limited solely to how nulls interact with switch, and does not affect the semantics of pattern matching at all. (I’m happy you’re happy, but it’s more important that you understand what exactly is being proposed.)
> This looks like it would be much better than the original proposal. It
> argues that null-acceptance derives from the context
I’m not sure where you get this. We’ve been pretty consistent from the beginning in saying that both context and patterns can have opinions about nullity, and the context always gets to express its opinions first, and can choose to not even pay attention to the pattern. Patterns have their own semantics, which are independent of context, and then contexts get to layer their semantics on top.
> It seems eminently reasonable, defendable and teachable to say that
> `var o` and `Object o` behave differently in a pattern context to a
Believe me, I have spent more hours thinking about this proposal than you’ll know. There’s even a trivial existence proof that this is viable, in that this is what Scala does, and the Scala work hasn’t blown up (at least, not because of this). But I am convinced that this would be the wrong move for Java.
The cost of this is subtle, but significant — `var` becomes a far more complicated concept; it is no longer mere type inference. It means that in some contexts, swapping var for a manifest type subtly changes the semantics, in extremely subtle ways that won’t be noticed at first and in ways that are likely to be persistently error-prone. (And for types that are not denotable and therefore var is your only choice, it means you’re locked into one particular null handling regime.) Essentially, this is a trade-off of “let’s make var way more complicated in a super-subtle way that ordinary users will never internalize in a million years”.
So, let’s say we made that trade; what do we get? I don’t think the return is there. You can’t banish the complexity of null handling, you can only moving it to where it is more surprising or increase the complexity of the mechanisms. Your proposal just moves it to var.
I’ll state again, too, that I think the “OMG the nulls will eat us in our sleep” fears are dramatically overblown. Before we set anything on fire, I think we should come to terms with why we think there is such a problem.
> As can be seen this really this isn't about null, but about making
> type patterns actually work consistently.
Type patterns do work consistently when you zoom out just a tiny bit, and recognize that pattern matching exists in a static type context. Suppose I have `record Box(Object) {}`. Then the pattern:
case Box(Object o):
is, in some sense is “one” pattern match; the inner pattern (Object o) can be statically determined to always succeed when the outer one does, and therefore the only dynamic test here is for Box-hood. So we test for Box-hood, conditionally cast, extract the contents, and *assign* it to o. Assignment doesn’t say “if the contents are null, fail”; it’s just that there *is no inner match here*. But in the pattern:
case Box(String s):
this is “two” pattern matches: even if the outer pattern matches, the inner might not. There are two dynamic tests, first for box-hood, and then for string-hood of the box contents. And the string-hood test is done as if by instanceof, which is where the null rejection comes from — nothing new here. It is not that type patterns are “inconsistent”; it is that some patterns are really just assignment, and this is identified on the basis of whether a dynamic test is needed or not. There’s a reason that `instanceof` and casting are “inconsistent” with each other respect to nulls, and that reason is flowing into the semantics of patterns. (Do the thought experiment of “what if instanceof passed nulls” or “what if checkcast rejected nulls”, and imagine what would break. A lot.)
Trust me, we’ve been through all of these proposals (and more); they all lead to their own corner cases, composition failures, refactoring anomalies, etc. that seem worse (in some cases, far worse) or are more complex, or both. Rather than tinkering with the rules, I think a better investment of your energy is trying to understand exactly _why_ you find the status quo so problematic that you are inclined to set it on fire. I think investing that effort in better understanding the problem, rather than trying to “solve” it, is far more likely to yield a useful result, and as a bonus, is far more likely to move the needle if there really is something there.
> If the type pattern says
> `Number n` it should always mean `n instanceof Number`
We’ve been through this already in a previous round. I get that you have decided that this is the fixed point of your interpretation of what type patterns mean, but the problem is just not as simple as you would like it to be. While it is theoretically possible to distort the design to adopt this as the founding principle, it just moves the damage elsewhere.
Perhaps this would help: you are saying “Please don’t set my understanding of `instanceof` on fire. I have a proposal so that you don’t have to do it, all you have to do is set `var` on fire.” You can see how this might not seem an obvious trade.
More information about the amber-dev
mailing list