pattern references, lambdas

elias vasylenko eliasvasylenko at gmail.com
Fri Jan 11 09:16:07 UTC 2019


Very interesting to hear where this is leading.

So to not constrain the number of elements extracted, and to allow the
components to be reduced back into a single result (or constrained number
of outputs) in one neat little linguistic construct, perhaps you're
alluding to something like allowing patterns to appear in place of lambda
parameters, deconstructing arguments in-place? Sounds comfortingly visually
similar to switch expressions.

    int eval(Node n) {
      return switchy(n)
        .casey(IntNode(int i) -> i)
        .casey(NegNode(Node n) -> -eval(n))
        .casey(AddNode(Node left, Node right) -> eval(left) + eval(right))
        .casey(MulNode(Node left, Node right) -> eval(left) * eval(right))
        .result();
      };
    }

Where `casey` must still take a "patternal interface" (or whatever else is
to represent a pattern in the type system) since the patterns applied to
arguments are partial, though there's no reason total patterns couldn't
appear in the place of the parameters of a normal functional interface
lambda.

Well that's probably enough of my ramblings. Thanks for the insight into
progress, looking forward to future writeups etc.

On Thu, 10 Jan 2019, 16:43 Brian Goetz <brian.goetz at oracle.com wrote:

> tl;dr: yes, we’re thinking about everything you raise here...
>
> > On Jan 10, 2019, at 9:56 AM, elias vasylenko <eliasvasylenko at gmail.com>
> wrote:
> >
> > It occurred to me that the current work on hashing out a proposal for
> > patterns <
> https://cr.openjdk.java.net/~briangoetz/amber/pattern-match.html>
> > (including deconstructor, static, and instance forms) also may imply some
> > other new concepts.
> > - "pattern references" as a complement to method references.
> > - "patternal interfaces" as a complement to functional interfaces,
> > containing an instance-pattern declaration rather than a method
> declaration.
>
> It’s possible that we may need these concepts, but its also possible that
> they are “forced consistencies.”  So I prefer to approach these things from
> the direction of what we want to express, rather than starting with the
> abstract concept.
>
> > I assume there is prior art to lean on, and that the concept of true
> > first-class patterns as a complement to first-class functions is nothing
> > novel to functional programmers ... so this is probably not new to many
> > people. I just wanted to put this out there while it's fresh in my mind
> and
> > hopefully see some public discussion on whether it's plausible for Java.
>
> Functional programmers are perfectly happy expressing a pattern on a
> target T with bindings (U,V) as a function from T to something like
> Optional<Tuple<U,V>> (for various spellings of “optional” and “tuple”.)
>
> Despite its obvious benefits, when considering pattern matching in the
> context of an object-oriented language, this formulation starts to look
> more like “a clever hack.”  Yes, you can represent patterns this way, but
> it’s pretty weak; you give up the ability to be part of the object model.
> A more Java-centric way to model a pattern is that a pattern is a _class
> member_.  It makes sense to talk about deconstruction patterns (the dual of
> constructors), static patterns (the dual of static methods), and instance
> patterns (the dual of instance methods.)  And for instance patterns, it
> makes sense to talk about abstract patterns and pattern overriding.
>
> Stay tuned for a more detailed writeup of these concepts.
>
> > So, consider the following.
> >
> >    items()
> >      .filter(SpecificItemType.class::isInstance)
> >      .map(SpecificItemType.class::cast)
> >      .forEach(itemHandler::doSomething);
> >
> > Classic example of a test and extract being decomposed into two separate
> > steps when really we want to do it all at the same time. But how do we do
> > this with the current pattern proposal? The best we can do, I think, is
> > just to burden the caller with a little more responsibility to manage the
> > plumbing.
> >
> >    items()
> >      .flatMap(i -> i instanceof SpecificItemType s ? Stream.of(s) :
> > Stream.empty())
> >      .forEach(itemHandler::doSomething);
>
> Correct, if you don’t want to do the work twice (and who would?), then
> some form of flatMap() is how you would express this.  But, pull on the
> string some more.  This example is less interesting because the pattern
> only produces one binding, the casted result.  What about a pattern that
> does deconstruction, such as `Person(var first, var last)`?  How would you
> even feed that downstream, since a stream expects a scalar?
>
> (Anybody tempted to say “duh, that’s why you should just do tuples” at
> this point, please see yourselves out.)
>
> The answer here is: only _you_ know what you intend to push downstream.
> Maybe its something that wraps all the bindings into a record, or maybe a
> subset of them, or maybe some other combination, such as `first + last`.
> You’re going to have to write that code.
>
> Note that you’d more likely flatMap to `Optional` as a carrier than
> `Stream`, since the arity of a match is going to be zero-or-one, and using
> Optional here will be much more efficient.  (It’s a sad accident that we
> can’t just overload flatMap(T -> Stream<T>) and flatMap(T ->
> Optional<T>).)
>
> > This isn't too awful, but it's hardly an earth-shattering improvement.
>
> Agreed.  It’s a starting point.  (Alternately, flatMap to:
> Optional.ofNullable( x instanceof P(a,b,c) ? f(a, b, c) : null))
>
> > I
> > think it would be a terrible shame if we couldn't just do something like
> > this:
> >
> >    items()
> >      .partialMap(SpecificItemType::instanceof)
> >      .forEach(itemHandler::doSomething);
>
> Sure, for one-in, one-out patterns, this seems pretty attractive, but this
> is the trivial case.  So pull on the string some more.  What’s the
> equivalent when the pattern has multiple bindings?  You will also need a
> function that, when applied to the multiple bindings, wraps them up and
> sends them downstream:
>
>     .partialMap(Person::match, (first, last) -> …)
>
> which is where you want to bring in the connection to functional
> interfaces, I presume.  You want a structural way to describe the shape of
> the “output” of the Person match.  Then you want to constrain that “output”
> shape to be the shape of the input to the function.  Think about how you
> might express such a constraint.
>
> > And we may also have e.g.
> >
> >    public interface BiPattern<T, R, S> {
> >      __Pattern T t match(R r, S s);
> >    }
>
> Yep, you’re appealing to the idea that “one or two outputs will be
> enough.”  But, I seriously doubt that; patterns will frequently have more
> outputs than that.
>
> You’re on a good track, but a lot more is needed here to make it fit.
> Then we have to consider whether the cost is worth it, or whether something
> more like your flatMap example, cleaned up a bit, is good enough.
>
> > So is this something that's been considered? Is any of it plausible?
> > Desirable?
>
> Considered: extensively.
> Desirable: definitely.
> Plausible: still thinking.
>
> > Lambdas are a similar story I suppose. For example, say we want to filter
> > our items based on the type, then on the presence of some kind of
> optional
> > content, and also then finally extract that content:
> >
> >    items()
> >      .partialMap(item (content) -> __let SpecificItemType(Optional(var
> > content)) = item)
> >      .forEach(contentHandler::doSomething);
>
> Right.  This is the general case, where you do a match, conditionally
> extract bindings, and then conditionally use those bindings as input to a
> function, which produces a scalar — and then represent that weird entity in
> the type system so you can expose it as a method on Stream.
>
> So yes, you’re on a track we’ve been following down, not entirely sure
> where it will lead.
>
>
>


More information about the amber-dev mailing list