Using pattern instead of Optionals

Brian Goetz brian.goetz at oracle.com
Mon Oct 15 13:27:09 UTC 2018


Lots of questions here, let me disentangle....

I don't want to rathole too much on the "where's my Elvis operator" 
tangent, but I'll just point out that we might consider an alternate 
`Optional` method that fuses `map` and `Optional.ofNullable()`.

     version = Computer.elvis(Computer::getSoundcard)
                       .elvis(SoundCard::getUSB)
                       ...

where Optional::elvis is:

     <U> Optional<U> elvis(Function<T,U> m)
         -> flatMap(v -> Optional.ofNullable(m.apply(v)));

This lets you get what you want with standard getters.  (And, that's the 
end of this digression on this list; if you want to bikeshed this idea, 
let's take that discussion to corelibs-dev.)

OK, now back to pattern matching.

1.  Will pattern matching help here?

It will help, a little.  As you point out, you can use `instanceof` to 
fuse the test and binding and chaining.  I'd add parens and reformat 
your example to make it more readable:

String version = (computer != null
                   && computer.getSoundcard() instanceof Soundcard soundcard
                   && soundcard.getUSB() instanceof USB usb)
                       ? usb.getVersion
                       : "UNKNOWN";

I think that's fine.  Pattern-ful instanceof is intended to play nicely with short-circuit booleans:

     boolean equals(Object o) ->
         o instanceof Me m
             && name.equals(m.name)
             && age == m.age;

Yours uses a similar expression as the conditional of a ternary; the 
only thing I didn't like about your example was that the formatting made 
it hard to tell where the conditional ended and the ternary began.

2.  Can I say instanceof var x

In theory, you could.  I suspect we'll want to ban `var x` and `_` as 
pattern operands of isntanceof, because they're just kind of silly.  
(Yes, its a sneaky way to introduce a new binding, but its a pretty 
silly way to say `true`.)

3.  Would `var x` match null?

Yes.  So it doesn't do what you want here anyway.

4.  Could I use deconstruction patterns here?

Yes, but it would probably be a distortion, at least as much as changing 
all your getters to return Optional.

The idea is that deconstructors are members, with declarations, just 
like constructors.  So they can be overloaded, just like constructors.  
In theory, you could have deconstructors for each property:

     public extractor Computer(SoundCard sc) { ... }
     public extractor Computer(DisplayCard dc) { ... }
     public extractor Computer(PowerSupply ps) { ... }

But, I think this would be a serious distortion, for at least two 
reasons.  We wouldn't write constructors like this; its a bad sign when 
your construction and deconstruction protocols don't line up.  And, an 
API like this, that overloaded a bunch of 1-arg deconstructors, 
essentially prevents clients from using `var` patterns in deconstruction:

     case Computer(var soundCard) // nope

Here, overload selection on deconstruction patterns would fail, because 
all three are applicable.  (This is not unlike overloading 
m(Function<T,U>) and m(Predicate<T>); you won't be able to call them 
with lambdas, because overload selection will fail.  Its a valid 
overloading, but one that is not nice to your clients.  (We issue a 
warning here at the declaration.))

5.  Could I write named extractors instead, to get around the 
overloading problem above?

YES!  And we believe that this idiom has been something missing from OO 
APIs for a long time, and we have just not noticed.

A pattern fuses a test with a conditional extraction. Today, we write 
method pairs like Class.isArray(), and Class.getComponentType().  You're 
only supposed to call the latter if the former says yes.  This coupling 
makes work for the implementor (deal with precondition failure, specify 
what happens on precondition failure, mark the methods as related, etc), 
and does nothing to prevent the user from getting it wrong (calling the 
second without calling the first.)  These are begging to be a single 
pattern.

If these were a single pattern, there's be no need to specify what 
happens on precondition failure, because there are no preconditions.  
And it can't be gotten wrong.

     if (x instanceof arrayClass(var componentType)) { ... }

In this case, arrayClass() is an _instance pattern_ on Class.

You could do the same with your Computer example:

     if (c instanceof withSoundcard(withUsb(var usb))) { ... }

Computer would have instance pattern `withSoundcard(Soundcard)`, and 
Soundcard would have instance pattern `withUsb(Usb)`.

This solves your use problem, but leads me to ask: is this a sensible 
API, or is treating patterns as "nullable getters" an abuse?  Let me lay 
out the story here, and you can decide for yourself.


Just as a class can have constructors, static methods, and instance 
methods, all of which are "method-like", a class can have deconstruction 
patterns, static patterns, and instance patterns. The latter are the 
dual of the former.

We expect that most patterns will be related to existing API elements in 
an obvious way.  For example, if you construct instances with 
constructors, you should be able to deconstruct them with deconstruction 
patterns:

     Foo f = new Foo(a, b);
     ...
     case Foo(var a, var b): ...

And, if you provide a ctor/dtor pair with the same API, it supports 
mechanical marshalling and cloning, such as:

     Foo clone(Foo f) {
         let Foo(var a, var b) = f;
         return new Foo(clone(a), clone(b));
     }

The syntactic similarity of construction and deconstruction is not 
accidental.

If your class exposes static factories instead of constructors, you can 
also expose static patterns:

     Foo f = Foo.of(a, b);
     ...
     case Foo.of(var a, var b);

Instance patterns are a little less obvious what they are the dual of.  
But as you hint at, they are the dual of "wither" methods:

     Foo f = foo.withCheese(CHEDDAR);
     ..
     case withCheese(var cheese): ...

More generally, they are the dual of builder methods.  We use builders 
for building complicated entities; we can use "unbuilders", with 
instance patterns, to reverse the process.



On 10/14/2018 11:08 PM, Tagir Valeev wrote:
> Hello!
>
> Just some thoughts. I recently revisited this Optional technetwork
> article from Java 8 times:
> https://www.oracle.com/technetwork/articles/java/java8-optional-2175753.html
>
> In the article it's assumed that we need a code like this:
>
> String version = "UNKNOWN";
> if(computer != null){
>    Soundcard soundcard = computer.getSoundcard();
>    if(soundcard != null){
>      USB usb = soundcard.getUSB();
>      if(usb != null){
>        version = usb.getVersion();
>      }
>    }
> }
>
> Which is indeed ugly. Nowever changing the API methods getSoundcard()
> and getUSB() to return Optionals (assuming that getVersion() never
> returns null) and migrating local variable computer to Optional as
> well we could write this:
>
> String version = computer.flatMap(Computer::getSoundcard)
>                            .flatMap(Soundcard::getUSB)
>                            .map(USB::getVersion)
>                            .orElse("UNKNOWN");
>
> I personally not a big fan of changing everything to optionals (this
> article even suggests to use optional fields!), because the resulting
> functional code is much less flexible. Also not always you are in
> position to update the existing API. Currently I often prefer
> null-checks.
>
> Currently you cannot express the original code with single expression
> without some ugly static methods, unless you can afford to call
> getters several times (and hope that they return stable result):
>
> String version = computer != null && computer.getSoundcard() != null &&
>                   computer.getSoundcard().getUSB() != null ?
>                   computer.getSoundcard().getUSB().getVersion() : "UNKNOWN";
>
> I realised that pattern matching allows you do to this:
>
> String version = computer != null &&
>                   computer.getSoundcard() instanceof Soundcard soundcard &&
>                   soundcard.getUSB() instanceof USB usb ?
>                   usb.getVersion() : "UNKNOWN";
>
> Is such pattern usage considered idiomatic? Could it be simplified?
> I'm not sure that `instanceof var soundcard` could be used here,
> because it seems that `var` would match null as well. Well, assuming
> that computer can be deconstructed into Soundcard which can be
> deconstructed into USB, we could write even simpler:
>
> String version = computer instanceof Computer(Soundcard(USB usb)) ?
>                                usb.getVersion() : "UNKNOWN";
>
> However this looks like an abuse of deconstruction: unlikely one have
> a constructor which creates a Computer from Soundcard only. Or
> probably we can create static named deconstructors like this (I saw
> something like this in some documents):
>
> String version = computer instanceof
> Computer.withSoundcard(Soundcard.withUSB(var usb)) ?
>                                usb.getVersion() : "UNKNOWN";
>
> Will this be possible?
>
> With best regards,
> Tagir Valeev.



More information about the amber-spec-experts mailing list