Member Patterns -- the bikeshed

Brian Goetz brian.goetz at oracle.com
Fri Apr 5 15:18:24 UTC 2024


Thanks for this more detailed explanation.

Here's one thing I would like to drill into, which I hinted at in my 
mail the other day: why we find "imperative" comforting.  I proposed two 
theories:

  - Looks like a constructor body in the mirror
  - Nominal association between binding and value is sometimes more 
clear than positional association

Indeed, your comment that "which I would choose likely depends on arity" 
suggests that you are mostly aiming at the latter. Assuming this is most 
of the answer, it leads me to ask the following questions:

1.  In a world where we had a more general mechanism for by-name 
{invocation,matching}, wouldn't we prefer that?  Let's say that by-name 
invocation looked like:

     new Point(x: 1, y: 2)

The logical companion at the use site would be:

     case Point(x: var a, y: var b):

and the logical companion at the match site would be:

     matches Point(x: this.x, y: this.y)

While I'm not ready to commit to this feature, it seems to me the 
possibility that we could have a broader way to associate names with 
values at various parentheses-bounded constructs suggest that inventing 
a fresh one, with more limited applicability, might not be ideal.

2.  Users can already simulate imperative with functional without a 
language feature, and indeed, can do so more flexibly because it's not 
all-or-nothing.  Suppose we had the following imperative dtor:

     pattern Foo(int a, int b, int c, ... int z) {
         a = this.a;
         b = /* really complicated computation */
         c = this.c;
         ... more trivial bindings ...
         match;
     }

Here, one binding is complex and the rest are trivial.  With functional, 
users can already do:

     pattern Foo(int a, int b, int c, ... int z) {
         var b = /* really complicated computation */
         match Foo(this.a, b, this.c, ...)
     }

Which is to say, if we need to use imperative logic to "outline" a 
complex calculation, the language provides features for doing so.  Now, 
a dtor with so many bindings (and worse, all of the same type) is at 
risk for getting "out of sync", but at this point I refer back to 
argument #1, which is that someday we may be able to provide nominal 
context for these expressions to prevent such errors, at which point we 
can write:

         match Foo(a: this.a, b: b, c: this.c, ...)

without having to have two linguistic ways to write a matcher (with the 
attendant "style wars".)





On 4/4/2024 4:18 PM, Guy Steele wrote:
>
>> On Apr 4, 2024, at 1:11 PM, Brian Goetz <brian.goetz at oracle.com> wrote:
>>
>> There's obviously some more discussion coming about "what is a 
>> pattern", but let me summarize the points on which we've asked for 
>> syntax feedback, and make another call (I can't believe I have to 
>> ask) for opinions here.
>> ...
>>
>> Body types.  There is the broad choice of "imperative vs functional"; 
>> within that, there are choices about "implicit failure" or "implicit 
>> success."  There is also how we indicate success and failure.  The 
>> suggested approach is functional, implicit failure, return means 
>> fail, success is indicated by `match patternName(BINDINGS)`.
>
> The draft proposal that Brian sent out on March 29, in the section and 
> subsections with these headings:
>
> ## Body types
> ### Success and failure
> ### Implicit failure in the functional approach
> ### Implicit success in the imperative approach
> ### Imperative is a trap
> ### Derive imperative from functional?
>
> laid out a version of the functional approach in which failure is 
> implicit, a version of the imperative approach in which success is 
> implicit, and an add-on to the functional approach that allows it to 
> be used in a way that is syntactically similar to the imperative 
> approach. But this was an incomplete presentation of a design space 
> that actually has more possibilities and potential symmetries.
>
> Here I undertake a complete retelling of an imperative and approach 
> and a functional approach and then compare them. An important 
> difference is that *I will assume a version of the imperative approach 
> in which failure, rather than success, is implicit*. The reason for 
> this is while we expect simple deconstructors always to succeed—and 
> that motivates us to make success implicit, to make deconstructors one 
> line shorter—that is not true for other kinds of patterns, and I think 
> it is good to mark pattern success explicitly no matter which approach 
> is used used.
>
> Here, then, is my retelling. As part of this retelling, I will explain 
> pattern-match success in terms of a new kind of reason for abrupt 
> completion: “a successful match with match results (z1, z2, …, zn)” 
> where each zk is some value.
>
>
> *An Imperative Approach (in which failure is implicit)*
>
> The parameters of a pattern declaration are definitely unassigned at 
> the start of the body of the declaration. They may be given values 
> through ordinary assignment. For expository purposes, let the names of 
> the parameters be v1, v2, …, vn.
>
> If execution of the body completes normally, or completes abruptly for 
> any reason other than a successful match, then the invocation of the 
> pattern results in a failed match. In particular, the statement 
> `return;` may be used in the body of a pattern declaration to indicate 
> failure to match.
>
> The statement `match;` (or, if you prefer, `match patternName;`, but I 
> will stick with the shorter form for now) indicates a successful 
> match. It may be used only within the body of a pattern declaration. 
> Execution of `match;` causes the body of the pattern declaration to 
> complete abruptly, the reason being a successful match with match 
> results (v1, v2, …, vn)—that is, the current values of the parameters 
> v1, v2, …, vn are used as the match results.
>
> It is a compile-time error if any of the parameters of a pattern 
> declaration is not definitely assigned at any `match;` statement.
>
> /Optional restriction:/ It is a compile-time error if the body of a 
> deconstructor pattern declaration can complete normally or contains a 
> `return;` statement. (This restriction would imply that a 
> deconstructor cannot fail to match. This restriction would not apply 
> to static or instance patterns.)
>
> Here is the Point deconstructor written in the imperative style.
> |class Point { int x, y; Point(int x, int y) { this.x = x; this.y = y; 
> } // Imperative style pattern Point(int x, int y) { x = that.x; y = 
> that.y; match; // Match success must be signaled explicitly } }|
> In this imperative style, the deconstructed body looks like the 
> “reverse" of the constructor body, with the sides of each assignment 
> swapped and `that` substituted for `this`—and, of course, the addition 
> of a `match` statement to signal success.
>
> *Special convenience feature: *Another form of the `match` statement 
> is provided for convenience:
> |match (e1, e2, …, en); |
> means
> |{ var t1 = e1, t2 = e2, …, tn = en; v1 = t1; v2 = t2; … vn = t1; match; }|
> where temporaries t1, t2, …, tn are fresh local variables that do not 
> occur elsewhere in the program. (It is a compile-time error if the 
> number of expressions does not match the number of parameters, or if 
> for any k the type of ek is not assignment-compatible with the 
> declared type of vk.)
>
> This allows the deconstructor for Point to be written this way instead 
> if desired:
> |// Imperative style, but using the extended `match` statement to 
> abbreviate a series of boilerplate assignments pattern Point(int x, 
> int y) { match (that.x, that.y); }|
>
>
> *Alternatively, a Functional Approach **(in which failure is likewise 
> implicit)*
>
> [This is very close to what Brian proposed, but I express it in the 
> same detailed terms that I used above to describe the variant 
> imperative approach that assumes failure is implicit.]
>
> If execution of the body completes normally, or completes abruptly for 
> any reason other than a successful match, then the invocation of the 
> pattern results in a failed match. In particular, the statement 
> `return;` may be used in the body of a pattern declaration to indicate 
> failure to match.
>
> For expository purposes, let the names of the parameters of the 
> pattern be v1, v2, …, vn.
>
> The statement `match (e1, e2, …, en);` (or, if you prefer, `match 
> patternName(e1, e2, …, en);`, but I will stick with the shorter form 
> for now) indicates a successful match, using the values of the 
> expressions e1, e2, …, en. It may be used only within the body of a 
> pattern declaration. Execution of `match (e1, e2, …, en);` causes the 
> body of the pattern declaration to complete abruptly, the reason being 
> a successful match with match results (z1, z2, …, zn), where z1, z2, 
> …, zn are the respective results of evaluating the expressions e1, e2, 
> …, en (working left-to-right). If evaluation of any en completes 
> abruptly, then evaluation of `match (e1, e2, …, en);` completes 
> abruptly for the same reason.
>
> It is a compile-time error if the number of expressions does not match 
> the number of parameters, or if for any k the type of ek is not 
> assignment-compatible with the declared type of vk.
>
> /Optional restriction:/ It is a compile-time error if the body of a 
> deconstructor pattern declaration can complete normally or contains a 
> `return;` statement. (This restriction would imply that a 
> deconstructor cannot fail to match. This restriction would not apply 
> to static or instance patterns.)
>
> Here is the Point deconstructor written in the functional style.
> |class Point { int x, y; Point(int x, int y) { this.x = x; this.y = y; 
> } // Functional style pattern Point(int x, int y) { match (that.x 
> that.y); } }|
>
> In the functional style, the `match` statement that signals success 
> looks somewhat like an invocation that provides desired values 
> corresponding to the declared parameters.
>
> The parameters of a pattern declaration are in fact declared local 
> variables that are definitely unassigned at the start of the body of 
> the declaration. They may be given values through ordinary assignment, 
> but need not be; the compiler will not complain just because a pattern 
> parameter goes unused as a local variable. One possible use for them 
> is to hold values intended to be match results while other values are 
> still being computed.
>
> *Special convenience feature: *Another form of the `match` statement 
> is provided for convenience:
> |match; |
> means
> |match (v1, v2, …, vn);|
> where v1, v2, …, vn are the names of the declared pattern parameters.
>
> This allows the deconstructor for Point to be written this way instead:
> |// Functional style, but using parameter variables for convenience to 
> stash intermediate match result values as they are computed pattern 
> Point(int x, int y) { x = that.x; y = that.y; match; }|
> It is a compile-time error if any of the parameters of a pattern 
> declaration is not definitely assigned at any `match;` statement.
>
>
> *Comparing These Imperative and Functional Approaches*
>
> The two approaches are described from different perspectives, and 
> suggest slightly different implementation techniques, but *they allow 
> the programmer to write exactly the same set of programs*. Assuming 
> reasonable compiler optimization of chained assignments and unused 
> local variables, *the resulting machine code should be the same in 
> either case*. Whether or not to use explicit assignment to the pattern 
> parameter variables becomes entirely a matter of taste. If the number 
> of parameters is, say, 4 or less, I would probably prefer to write a 
> pattern in the functional style, to cut down on clutter. But if the 
> number of parameters is, say, 7 or more, I would probably prefer to 
> write a pattern in the imperative style, to make it easier to see that 
> each match result has been assigned to the correct parameter. In 
> between, my mileage might vary.
>
> *It would seem, then, from these explanations and examples, that we 
> could choose either of these models as the “official” explanation of 
> how the bodies of pattern declarations work.* I actually thought that 
> for a little while. It does seem that either is easily derived from 
> the other by introducing a plausible “special convenience feature”.
>
> *But* if we want to be able to use the SAP (single-abstract-pattern 
> interfaces) feature that Brian introduces toward the end, in his section
>
> ## Pattern lambdas
>
> so that patterns can be expressed as lambda expressions, then *the 
> functional approach is clearly the better choice*. To see why, 
> consider his example:
> |interface Converter<T,U> { pattern(T t) convert(U u); }|
> |Converter<Integer, Short> c =     i -> {         if (i >= 
> Short.MIN_VALUE && i <= Short.MAX_VALUE)                match 
> Converter.convert((short) i);};|
> This lambda expression is, of course, written in the functional style. 
> But watch what happens if we try to write it in the imperative style:
> |Converter<Integer, Short> c =i -> {          if (i >= Short.MIN_VALUE 
> && i <= Short.MAX_VALUE) {u = (short) I; // PROBLEM: u is not in scope 
> match; } }; |
> The problem is that the parameter name `u` is declared in the SAP 
> interface `Converter` but is not in scope within the lambda 
> expression. This, I think, is reason enough to regard the functional 
> approach as the “official explanation” of what is going on, because, 
> as with methods and the way they bind method parameters to argument 
> values, the baseline mechanism in Java for establishing correspondence 
> between parameters and values is order within a sequence rather than 
> matching of parameter names.
>
> So, in the end, I recommend adopting the functional approach, but I 
> also recommend adopting the “special convenience feature” so that the 
> syntactic style of the imperative approach can be used in certain 
> common cases.
>
> —Guy
>
>
>
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-spec-observers/attachments/20240405/0db84ca2/attachment-0001.htm>


More information about the amber-spec-observers mailing list