Pattern method names based on existing library conventions [was Re: Pattern matching for nested List with additional size constraints]

Stephen Colebourne scolebourne at joda.org
Thu Jan 28 00:21:44 UTC 2021


On Wed, 27 Jan 2021 at 20:03, Brian Goetz <brian.goetz at oracle.com> wrote:
>       if (p instanceof Profile(Rules(List.of(var rule, ...)))) {...}

With my library design hat on, I don't think "of" is a suitable method
name here. "of" is strongly linked to creation, not extraction (see
also "valueOf" on some other classes). Looking further, I found three
different kinds pattern method that benefit from different treatment
at the use-site:


1) Factory methods that should be constructors

Some factory methods exist simply because a constructor was not an
acceptable alternative - LocalDate.of(int, int, int),
Integer.valueOf(int) and List.of(T...) being three examples for
different reasons. Given this, it seems desirable that the language
should allow *some* pattern methods to have a use-site syntax that
looks exactly like a deconstructor.

  if (p instanceof Profile(Rules(List(var rule, ...)))) {...}
  if (p instanceof LocalDate(var year, month, day)) { ..}
  if (p instanceof Integer(var value)) { ..}
  if (p instanceof Optional(var value)) { ..}
  if (p instanceof Optional()) { ..}
  for (Map.Entry(var key, var value) : map.entrySet() {...}

In all these situations, the binding is effectively taking the raw
contents and binding them, as records do, and that is what the
use-site syntax should be.


2) Extract everything and Transform.

This is where the inbound factory method performs a meaningful
transformation, not just a straightforward assignment. A typical
example is  `LocalDate.ofYearDay(int, int)` vs `LocalDate.of(int, int,
int)`. The pattern method will typically always match, but it doesn't
have to. Examples so far suggest these kind of transformation
factories would look like:

  if (p instanceof LocalDate.ofYearDay(var year, var dayOfYear)) { ..}

But I'm not creating a LocalDate, I'm extracting it.

It seems to me that there is already a method prefix that already
represents something like "extract and transform" - the method prefix
"as". eg. "I want the LocalDate as a YearDay"


3) Check and Extract.

This is like the `Rule.dynamic(var dc)` example in this thread, where
there is not always a match. Here, there are two distinct parts to the
problem - matching and extraction, but the existing proposed syntax
merges those two parts:

  if (p instanceof Map.withMapping(key)(var value)) {...}

Again, I don't think "withMapping" works well as a method name. Nor do
I believe this flows well when trying to read what it says.

But it seems to me that there is already a boolean method on Map that
performs the desired match. It is called `containsKey`. I believe this
will be true of the vast majority of pattern methods, eg.
`String.contains()`, `Class.isArray()` and many more. Typically these
are `isXxx()` methods.

So, my key observation is that pattern methods should be building on
the existing knowledge developers have of libraries by using these
existing boolean method names in patterns.


Proposal:

Pattern methods should be based on the *existing* method names
developers are already familiar with, with multi-step match & bind
where appropriate.

Instead of this:
  if (p instanceof LocalDate.ofYearDay(var year, var dayOfYear)) {...}
I propose:
  if (p instanceof LocalDate(asYearDay(var year, var dayOfYear)) { ..}

Instead of this:
  if (p instanceof Map.withMapping(key)(var value)) {...}
I propose:
  if (p instanceof Map(containsKey(key).get(var value)) {...}

the regex example:
  if (str instanceof matches(REGEX).groups(var match1, var match2)) { ..}

the JSON example:
switch (doc) {
    case hasKey("firstName").asString(var first) &
         hasKey("lastName").asString(var last) &
         hasKey("age").asInt(var age) &
         hasKey("address").asObject(
                 hasKey("city").asString(var city) &
                 hasKey("state").asString(var state)
         ) & ...
           -> ...
 }

and for the example from this thread:
  if (p instanceof Profile(Rules(List(isDynamic(var dc), ...))) {

Notes:
- the "as" method prefix is normally used when the match does not
fail, but doesn't have to
- the "is" method prefix is used when the match may fail
- this approach allows one pattern match method to have many different
associated pattern binding methods (in the first example, "get()" is
not special, it is just a method name)
- a combined match and bind pattern method is still allowed (the last
example, which is a bit like a cast)
- the JSON example is more like how a library writer might want to
separate the responsibilities (one method for "is there a key" and
many for "what type is it" which could perhaps throw with a meaningful
debug error)

Conceptually, `_.containsKey(key).get(var value)` can be thought of as
conjoined. A bit like a call to `containsKey` followed by a call to
`get` on the same target, but with some linked context. Of course, it
doesn't have to be implemented that way, but it is possible that the
declaration site might look something like:

 public class Map {
  public default pattern containsKey(K key).get(V value) ....
 }

with an implementation that just calls on to the existing two methods
of the same name.

Stephen


More information about the amber-dev mailing list