Varargs Record Patterns, Literal Record Patterns and Let Expressions vs Member Patterns

Clement Cherlin ccherlin at gmail.com
Thu Apr 4 20:22:09 UTC 2024


I was initially extremely excited for Member Patterns. I've been following
the discussion closely, and writing examples of how member patterns might
work in practice. I've come to the uncomfortable realization that there is
a lot of complexity involved, but I'm not seeing as much clear benefit as
I'd initially anticipated, and there are some simpler alternatives that
might deliver almost as much value.

For example, I can already "deconstruct" an Optional with a pattern switch.

for (var o : List.of(Optional.of("Hello!"), Optional.empty())) {
    switch (o.orElse(null)) {
        case null -> System.out.println("Empty!");
        case String s -> System.out.println(STR."Present: '\{s}'");
    }
}

Output:
> Present: Hello!
> Empty

Similar logic applies to any method that returns either a value or null,
including Class.getComponentType()

for (var c : List.of(String[].class, String.class)) {
    switch (c.getComponentType()) {
        case Class<?> componentClass -> System.out.println(STR."Array
\{componentClass}[]");
        case null -> System.out.println(STR."Not an array class: \{c}");
    }
}

Output:
> Array class java.lang.String[]
> Not an array class: class java.lang.String

So the value is most apparent to me in the cases where I want to
destructure a varargs result into multiple bindings, such as regular
expression patterns with groups, or I want to distinguish successfully
returning null from match failure (as in map.get()).

Riffing on the example from the bikeshed announcement:

record Match(String match, String... groups) {}

static Match find(Pattern p, String candidate) {
    Matcher matcher = p.matcher(candidate);
    if (matcher.find()) {
        return new Match(matcher.group(),
                IntStream.range(1, matcher.groupCount() + 1)
                        .mapToObj(matcher::group)
                        .toArray(String[]::new));
    }
    return null;
}

Creating the record is easy, but deconstructing it in today's Java is
*very* awkward.

Pattern A_DOT = Pattern.compile("a.");
Pattern BS_AND_CS = Pattern.compile("(b+)(c+)");

switch ("abcdefg") {
    case String s when find(BS_AND_CS, s) instanceof Match(var m, var gs)
-> {
        var bs = gs[0]; var cs = gs[1];
        System.out.println(STR."\{m}: \{bs}, \{cs}");
    }
    case String s when find(A_DOT, s) instanceof Match(var m, _) -> {
        System.out.println(m);
    }
    default -> {}
}

Ew.

However, the "ew" could be considerably reduced by providing an alternative
to postfix binding with "instanceof", and vararg patterns (which don't
require member patterns).

Consider a let expression:

let <record pattern> = <candidate>

as equivalent to

<candidate> instanceof <record pattern>

including the fact that the expression evaluates to a boolean, and flow
scoping of any bindings.

With let and varargs patterns, I'd have something much less awkward:

switch ("abcdefg") {
    case String s when let Match(var m, var bs, var cs) = find(BS_AND_CS,
s) ->
        System.out.println(STR."\{m}: \{bs}, \{cs}");
    case String s when let Match(var m) = find(A_DOT, s) ->
        System.out.println(m);
    default -> {}
}

It's still more verbose than "case BS_AND_CS.find(var m, var bs, var cs)"
but it's within the realm of practicality.

For the next example, define a variation of Optional that permits null
values:

record Option<T>(T value, boolean isPresent) {
    public Option {
        if (value != null) isPresent = true;
    }
    public static<T> Option of(T value) { return new Option(value, true);
    public static<T> Option empty() { return new Option(null, false);
}

public static <K,V> Option<V> getOption(Map<? super K, V> map, K key) {
    return map.containsKey(key) ? Option.of(map.get(key)) : Option.empty();
}

This, with literal values in record patterns, enables ergonomic checking of
map contents:

switch (getOption(map, key)) {
  case Option(null, true) -> log.warn("Value for key \{key} was null");
  case Option(var value, true) -> doSomethingWith(value);
  default -> log.warn(STR."Key \{key} not present");
}

Barely more verbose than the alternative with member patterns:

switch (map.getOption(key)) {
    case Option.isNull() -> log.warn("Value for key \{key} was null");
    case Option.isValue(value) -> doSomethingWith(value);
    default -> log.warn(STR."Key \{key} not present");
}

I know defining new records for new use cases is considerably less than
ideal. But I also think "let" expressions, varargs record patterns, and
literals in record patterns could deliver 80% of the value of member
patterns, with considerably less linguistic complexity. Further, the
individual features can be implemented independently.

I could of course be completely off base here. Feedback eagerly welcomed.

Cheers,
Clement Cherlin
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-spec-comments/attachments/20240404/124baf5a/attachment-0001.htm>


More information about the amber-spec-comments mailing list