Array patterns (and varargs patterns)

Remi Forax forax at univ-mlv.fr
Sat Sep 10 09:48:20 UTC 2022


> From: "Brian Goetz" <brian.goetz at oracle.com>
> To: "amber-spec-experts" <amber-spec-experts at openjdk.java.net>
> Sent: Saturday, September 10, 2022 2:16:15 AM
> Subject: Re: Array patterns (and varargs patterns)

> John pulled a nice Jedi-mind-trick on me, and pointed out that we actually have
> two creation expressions for arrays:

> new Foo[n]
> new Foo[] { a0, .., an }

> and that if we are dualizing, then we should have these two patterns:

> new Foo[] { P0, ..., Pn } // matches arrays of exactly length N
> new Foo[P] // matches arrays whose length match P

> but that neither

> new Foo[] { P, Q, ... } // previous suggestion
> nor
> new Foo[L] { P, Q } // current suggestion

> correspond to either of those, which suggests that we may have prematurely
> optimized the pattern form. The rational consequence of this observation is to
> do

> new Foo[] { P0, ..., Pn } // matches arrays of exactly length N

> now (which is also the basis of varargs patterns), and once we have constant
> patterns (which are kind of required for the second form to be all that
> useful), come back for `Foo[P]`.
I like this proposal, it offers a clean separation between the array pattern and a future spread pattern (or whatever when end up calling it). 

Rémi 

> On 9/6/2022 5:11 PM, Brian Goetz wrote:

>> We dropped this out of the record patterns JEP, but I think it is time to
>> revisit this.

>> The concept of array patterns was pretty straightforward; they mimic the nesting
>> and exhaustiveness rules of record patterns, they are just a different sort of
>> container for nested patterns. And they have an obvious duality with array
>> creation expressions.

>> The main open question here was how we distinguish between "match an array of
>> length exactly N" (where there are N nested patterns) and "match an array of
>> length at least N". We toyed with the idea of a "..." indicator to mean "more
>> elements", but this felt a little forced and opened new questions.

>> It later occurred to me that there is another place to nest a pattern in an
>> array pattern -- to match (and bind) the length. In the following, assume for
>> sake of exposition that "_" is the "any" pattern (matches everything, binds
>> nothing) and that we have some way to denote a constant pattern, which I'll
>> denote here with a constant literal.

>> There is an obvious place to put this (optional) pattern: in between the
>> brackets. So:

>> case String[1] { P }:
>> ^ a constant pattern

>> would match string arrays of length 1 whose sole element matches P. And

>> case String[] { P, Q }

>> would match string arrays of length exactly 2, whose first two elements match P
>> and Q respectively. (If the length pattern is not specified, we infer a
>> constant pattern whose constant is equal to the length of the nested pattern
>> list.)

>> Matching a target to `String[L] { P0, .., Pn }` means

>> x instanceof String[] arr
>> && arr.length matches L
>> && arr.length >= n
>> && arr[0] matches P0
>> && arr[1] matches P1
>> ...
>> && arr[n] matches Pn

>> More examples:

>> case String[int len] { P }

>> would match string arrays of length >= 1 whose first element matches P, and
>> further binds the array length to `len`.

>> case String[_] { P, Q }

>> would match string arrays of any length whose first two elements match P and Q.

>> case String[3] { }
>> ^constant pattern

>> matches all string arrays of length 3.

>> This is a more principled way to do it, because the length is a part of the
>> array and deserves a chance to match via nested patterns, just as with the
>> elements, and it avoid trying to give "..." a new meaning.

>> The downside is that it might be confusing at first (though people will learn
>> quickly enough) how to distinguish between an exact match and a prefix match.

>> On 1/5/2021 1:48 PM, Brian Goetz wrote:

>>> As we get into the next round of pattern matching, I'd like to opportunistically
>>> attach another sub-feature: array patterns. (This also bears on the question of
>>> "how would varargs patterns work", which I'll address below, though they might
>>> come later.)

>>> ## Array Patterns

>>> If we want to create a new array, we do so with an array construction
>>> expression:

>>> new String[] { "a", "b" }

>>> Since each form of aggregation should have its dual in destructuring, the
>>> natural way to represent an array pattern (h/t to AlanM for suggesting this)
>>> is:

>>> if (arr instanceof String[] { var a, var b }) { ... }

>>> Here, the applicability test is: "are you an instanceof of String[], with length
>>> = 2", and if so, we cast to String[], extract the two elements, and match them
>>> to the nested patterns `var a` and `var b`. This is the natural analogue of
>>> deconstruction patterns for arrays, complete with nesting.

>>> Since an array can have more elements, we likely need a way to say "length >= 2"
>>> rather than simply "length == 2". There are multiple syntactic ways to get
>>> there, for now I'm going to write

>>> if (arr instanceof String[] { var a, var b, ... })

>>> to indicate "more". The "..." matches zero or more elements and binds nothing.

>>> <digression>
>>> People are immediately going to ask "can I bind something to the remainder"; I
>>> think this is mostly an "attractive distraction", and would prefer to not have
>>> this dominate the discussion.
>>> </digression>

>>> Here's an example from the JDK that could use this effectively:

>>> String[] limits = limitString.split(":");
>>> try {
>>> switch (limits.length) {
>>> case 2: {
>>> if (!limits[1].equals("*"))
>>> setMultilineLimit(MultilineLimit.DEPTH, Integer.parseInt(limits[1]));
>>> }
>>> case 1: {
>>> if (!limits[0].equals("*"))
>>> setMultilineLimit(MultilineLimit.LENGTH, Integer.parseInt(limits[0]));
>>> }
>>> }
>>> }
>>> catch(NumberFormatException ex) {
>>> setMultilineLimit(MultilineLimit.DEPTH, -1);
>>> setMultilineLimit(MultilineLimit.LENGTH, -1);
>>> }

>>> becomes (eventually)

>>> switch (limitString.split(":")) {
>>> case String[] { var _, Integer.parseInt(var i) } -> setMultilineLimit(DEPTH, i);
>>> case String[] { Integer.parseInt(var i) } -> setMultilineLimit(LENGTH, i);
>>> default -> { setMultilineLimit(DEPTH, -1); setMultilineLimit(LENGTH, -1); }
>>> }

>>> Note how not only does this become more compact, but the unchecked
>>> "NumberFormatException" is folded into the match, rather than being a separate
>>> concern.

>>> ## Varargs patterns

>>> Having array patterns offers us a natural way to interpret deconstruction
>>> patterns for varargs records. Assume we have:

>>> void m(X... xs) { }

>>> Then a varargs invocation

>>> m(a, b, c)

>>> is really sugar for

>>> m(new X[] { a, b, c })

>>> So the dual of a varargs invocation, a varargs match, is really a match to an
>>> array pattern. So for a record

>>> record R(X... xs) { }

>>> a varargs match:

>>> case R(var a, var b, var c):

>>> is really sugar for an array match:

>>> case R(X[] { var a, var b, var c }):

>>> And similarly, we can use our "more arity" indicator:

>>> case R(var a, var b, var c, ...):

>>> to indicate that there are at least three elements.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-spec-observers/attachments/20220910/dfeaced0/attachment-0001.htm>


More information about the amber-spec-observers mailing list