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