Member Patterns -- the bikeshed
Brian Goetz
brian.goetz at oracle.com
Thu Apr 4 14:04:07 UTC 2024
>
> But taking the point of view of such a user, I just don’t see the
> need to introduce a new notion of “carrier" to explain the set of
> match results (an ordered sequence) from a pattern, just as I
> don't see any need to introduce a new notion of “carrier" to
> explain the set of arguments (an ordered sequence) to a method.
>
>
> For me the notion of carrier has several advantages:
> - it is easy to explain, a carrier is like an anynous record or a
> record with a predefined name, so this an object like any other normal
> object
> - the syntax is to close a type declaration, so the syntax can be
> extended to add '!' or '?' to signal if the pattern is optional or
> not, with the caveat that it only works well if we actually introduce
> '!' and '?' in the langage.
Yes, this is the old "it's just a method with multiple return" / "method
that returns Optional<Tuple>" analogy. While comfortable-seeming,
because it builds on things the user is already familiar with, this
analogy is flawed. Embracing a flawed but familiar model might help
users in the first five minutes, but it will damage the language forever
after.
Calling it something new like "carrier" has the obvious disadvantage
that it appears to be like a record, but isn't a record. This will
cause endless confusion. If we instead call these "anonymous records",
which is at least more honest (we've been through this flavor of the
design, it felt cool at first, but started to decay almost instantly),
you end up trying to reify the binding list in a way that we
deliberately _don't_ reify a method parameter list.
Scala's "function that returns Optional<Tuple>" is a glass that is half
full and half empty. It relies on generous structural magic, and
therefore does not fully live within the type system. And Scala's
patterns are less ambitious; they are much more "static", sitting at the
periphery of the object model.
>
>
> And I suggest that in a Java-based model where patterns are
> regarded as duals of methods, the same observations apply to
> sequences of match results. There is no need for such a sequence
> to be an object, or even to be given a special name such as
> “carrier”. The representation of such a sequence at run time is
> not the programmer’s concern, and that in turn may make it easier
> to allocate them on the stack rather than the heap in some
> situations. All I care about as a user of patterns is that I
> supply a match candidate, a pattern that does not fail produces an
> ordered sequence of match results, and those results are then
> bound, in order, to variables I specify at the point of pattern use.
>
>
> Patterns are not dual of methods, pattern deconstructors are dual of
> methods, but this is a special case.
Perhaps in Remi-world, Remi-patterns are something else. But that's not
the feature being designed here.
The design center here is that patterns are the dual of _aggregative_
methods (methods that takes more primitive ingredients and produce
something more abstract), like "make me a list from these elements",
"make me a point from this x and y", or "make me a class for the array
type whose component type is X". They are not merely "methods that
return multiple values", nor are they "methods that might fail". The
method to which they are dual need not actually exist (e.g., a pattern
that describes a regex match need not have a partner which generates
conformant strings, but it could).
If you disagree about the design center, you have two choices:
- Agree to disagree, and help design the best feature within the
planned design center
- Be honest that you are advocating to throw the design in the
garbage, and want to replace it with something else (likely, something
not as fully worked through), rather than merely offering a "tweak" to
the syntax
The bar for the latter is very, very high. You should make your case
directly, honestly, persuasively, and completely, and you should be
prepared that you may still not convince people, in which case it is
back to choice A.
Even if you are not successful, there is value in trying; the value is
in forcing us to come to a clearer statement of what the design center
is. We will have to explain this to others, so refining this story is
valuable.
> A pattern not only have a sequence of match results, it can have
> parameters too.
> For example, I may want to introduce an instance pattern asInteger()
> in java.lang.String that works like Integer.parseInt() but not match
> instead of throwing an exception if the string does not represent an
> integer.
So, this is a good illustration of the dangers of "method-think" here.
There is exactly one correct, obvious name for this pattern:
"Integer::toString". Except that at first, to almost everyone, it will
not seem correct and not seem obvious.
When all we had for designing the pair of conversions `int <--> String`
was methods, we modeled them as arbitrarily distinct, unrelated
methods. Going from int -> String was easy, since there was an obvious
way to do it and it worked for all integers. Going the other way is
harder, because it is partial, and because the language didn't offer a
canonical way to reflect partiality, we had to invent one, and every
such method did its own thing (maybe it returns a default; maybe it
throws; etc.) The API author had to make up TWO names, and we all know
naming is hard. Worse, the two methods toString and parseInt were not
obviously related, which meant that they were not discoverable from each
other. Worse still, the arms-length relationship between them meant
that they could easily gratuitously diverge.
When this is all we could do, we did it, and didn't realize there was
something better. But there is something better, and once you see it,
it is so blindingly obvious that you would not think to go back.
If you name the pattern `toString`, then:
- The two are easily discoverable from each other;
- The author will naturally align the semantics of the two, as they
are obviously two sides of the same coin;
- Promotion of int to String (aggregation), and the recovery of that
int from a candidate String (destructuring), look the same:
if (aString instanceof Integer.toString(int i)) { ... }
This evokes the Pattern Question: "could this string have come from
Integer.toString(i) for some i, and if so, please give me an `i` for
which this is the case."
If you encourage people to keep thinking about parseInt as a mere
method, they will continue to write dramatically worse APIs, which are
harder to write, harder to use, harder to read, and result in gratutious
asymmetries.
> I may also want that pattern to decode hexadecimal
Then find the duality. Converting from int to hex string could
reasonably be a method called toHexString, and there's your pattern:
if (aString instanceof Integer.toHexString(var i)) { ... }
> I want my pattern to also takes a radix as parameter. In that case, my
> pattern asInteger() has an int value as match result and has an int
> radix as parameter.
Here, you are saying "I want patterns with input parameters back." And I
agree that they were there for a reason. I think there is a better way
to bring back that functionality, and we can talk about it when we put
the core feature to rest.
> Using the carrier syntax, it's something like
Please stop pretending that this is a mere syntax choice. This is a
complete reinterpretation of what patterns are. You want patterns to
"just" be "conditional methods that can return multiple things." I get
how that seems a useful feature, but if that's what you want, then the
burden is on you is to be honest about it and to convince us to make a
radical change of direction. The mere existence of "here is some useful
code I could write with my direction" is barely even a start at that.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-spec-experts/attachments/20240404/5a9f8aae/attachment-0001.htm>
More information about the amber-spec-experts
mailing list