Feedback: Guards and static/instance pattern declarations

Brian Goetz brian.goetz at oracle.com
Sun Jan 24 16:09:22 UTC 2021


> The semantic approach of turning boolean expressions into guard
> patterns seems reasonable. But it seems to me that the goal should be
> to integrate boolean expressions and patterns as completely as
> possible, rather than trying to make them disjoint.

> // this is the current proposal
> case NumBox(int i) & true(i == 0):
> case NumBox(int i) & true(i > 6) & true(i < 10):
> 
> // this seems much more desirable
> case NumBox(0):
> case NumBox(int i) && i > 6 && i < 10:
> case NumBox(int i) && (i < 6 || i > 10):


Yes, this is the thing that one would “obviously want” after a first look at the problem.  And indeed, we thought that way at first too, and we spent a fair amount of time exploring this direction. And, it may even still be technically possible, but what I learned is that I am not sure we’d like the result (or the constraints) if we went all the way there.  But the reasons are not obvious from trivial examples such as these; in the trivial examples it works great, so OF COURSE that seems more desirable.  

> *  true(<booleanExpr>) and false(<booleanExpr>) seem like overkill
> ("new stuff wants to look new”)

I was sure someone would level that accusation, and you did not disappoint :)  But that’s not the motivation here.  

A better way to think about this distinction is it is like the “bun” in a stream pipeline.  Recall that when we did streams, there was a strong desire at first (including, on my part) to just expose the stream methods on collections, rather than requiring a “bun” where you have to exit from Collection World and enter Stream World.  The choice to require the .stream() call was a painful decision to make, full of self-about about what we were subjecting Java developers to, and much ink was spilled about the “unneeded verbosity".  In hindsight, though, it was “obviously” the absolute slam-dunk right move.  

The essence of the “bun” decision was that there was a transition in the programming model; collections are eager, mutative, data-centric , and streams are  lazy, computation-centric.  The programming models were different, so having a clean transition between them made sense, not because Streams were “new”, but because they were qualitatively different.  This decision turned out to have paid significant dividends beyond collections.  (At first, our desire to integrate these behaviors into collections was so powerful it blinded us to the much more powerful abstraction of Stream; only when we “gave into the bun” did we see this!  Kind of scary in hindsight.) 

The same is true, at least qualitatively, between patterns and guard expressions.  Patterns are templates for behavior, and their evaluation model is different from expressions.  Even absent syntactic ambiguities, it seems likely to lead to confusion when we get beyond the simplest examples.  And, there are syntactic forms that are currently in the intersection of the pattern and expression grammar (e.g., `Foo(), null, and possibly other constant literals`).  Even if we distorted the syntax to avoid these (which might generate more complaints about clunky syntax), ambiguities exist not only in grammars, but in our interpretation of programs.  The “bun” is a useful demarcation for streams, and while I totally understand how `true` seems a little heavy handed, I think its a useful demarcation here too.  

In an earlier set of questions about pattern matching, you asked “why not just have an if-match form, rather than make pattern matches full expressions.”  The response gave a number of examples, but the summary is that “we win when we lean into composition; we lose when we lean against it."

The “guards at the end” approach (whether conjoined with && or `when`) is another example of anti-compositional thinking in action.  Just as “if match” shunts the pattern to one corner of the if-else, and doesn’t allow it to be combined with other logic, treating guards as things we tack onto the tail of a case (which again, might seem the “obvious” move at first) means that we are constrained to execute all the patterns and then all the guards, rather than allow them to compose naturally in the order the programmer intends.  

> * the use of & is nasty given its meaning of evaluating both sides

Yeah, the both-sides thing is a concern.  

> By introducing a pattern context with a leading keyword, the rule
> would be that everything to the right of the keyword is interpreted
> using pattern context rules. Params would be used to control the
> boundaries of the pattern context if needed.

This is exactly the anti-compositional result I was discussing above.  Once you exit pattern-world, you can’t get back.  But, if you look at the sketches of examples for, say, JSON parsing, you want to be able to get back.  Because, composition.  

> it would be better to add case expressions
> (Ideally you'd drop support for type patterns after instanceof in Java
> 17 - I'm sure people would cope since it has only been there a few
> months)

I’m a little confused why we’re even talking about this; this ship has sailed?

> A separate part of the discussion is around how to use (and define)
> pattern methods that take an input argument. I can see why you'd like
> to add them to the language, but most of the examples provided so far
> tend to make code less readable, not more. I'm not yet convinced that
> user-defined patterns will be net positive to the daily use of the
> language.

I’m confident you’ll get there!  Even the most obvious examples (regular expressions, Map::get, JSON parsing) are pretty compelling, and you would stumble across them after only a short time using this stuff.  Do you really want to write a new “method” for every new regular expression you want to match?  That will get tired by the second day, I promise you.  

I get that this can be a lot to take in; if you haven’t been thinking about the problem for years, it may all seem very YAGNI.  But, there’s a deeper thing here beyond the surface features — we’ve been in such pain for the lack of these features for years, that we don’t even notice how painful it is that decomposition is completely ad-hoc and different from how we express aggregation, that conditional extraction is not compositional, that encapsulation is a unnecessarily one-way street.  

It is fair to wonder whether this is all too much, but it would be better to use that energy to imagine what you can do with this — and what the boundaries of that are, so they can be extended before the model becomes too set.  

> case regex(REGEX_PATTERN)(var regexMatch1, var regexMatch2):
> 
> I'd like to suggest an additional keyword between the two args-lists
> makes it much more palatable to read. Here is one possible way:

I appreciate the suggestion, but I think it’s premature to start nigling on the syntax of this at this point.  (Unless, you already are 100% convinced the model is 100% right, and you’re just trying to accelerate time-to-integration?)  The purpose of this document was not to fire the bikeshed-starting-gun (though I know its hard to resist), but to help people understand where we’re headed, and why.  Until you’re 100% on board with the where and why, I think getting caught up on the syntax is probably getting in the way of seeing the bigger picture.




More information about the amber-dev mailing list