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-experts/attachments/20240405/0db84ca2/attachment-0001.htm>
More information about the amber-spec-experts
mailing list