Member Patterns -- the bikeshed
Guy Steele
guy.steele at oracle.com
Thu Apr 4 20:18:28 UTC 2024
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/20240404/e79cb213/attachment-0001.htm>
More information about the amber-spec-experts
mailing list