pattern references, lambdas

Brian Goetz brian.goetz at
Thu Jan 10 16:43:22 UTC 2019

tl;dr: yes, we’re thinking about everything you raise here...

> On Jan 10, 2019, at 9:56 AM, elias vasylenko <eliasvasylenko at> wrote:
> It occurred to me that the current work on hashing out a proposal for
> patterns <>
> (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