Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Null-safe method chaining with Optional

Tags:

java

guava

Guava's Optional pattern is great, as it helps remove the ambiguity with null. The transform method is very helpful for creating null-safe method chains when the first part of the chain may be absent, but isn't useful when other parts of the chain are absent.

This question is related to Guava Optional type, when transformation returns another Optional, which asks essentially the same question but for a different use case which I think may not be the intended use of Optional (handling errors).

Consider a method Optional<Book> findBook(String id). findBook(id).transform(Book.getName) works as expected. If there is no book found we get an Absent<String>, if there is a book found we get Present<String>.

In the common case where intermediate methods may return null/absent(), there does not seem to be an elegant way to chain the calls. For example, assume that Book has a method Optional<Publisher> getPublisher(), and we would like to get all the books published by the publisher of a book. The natural syntax would seem to be findBook(id).transform(Book.getPublisher).transform(Publisher.getPublishedBooks), however this will fail because the transform(Publisher.getPublishedBooks) call will actually return an Optional<Optional<Publisher>>.

It seems fairly reasonable to have a transform()-like method on Optional that would accept a function which returns an Optional. It would act exactly like the current implementation except that it simply would not wrap the result of the function in an Optional. The implementation (for Present) might read:

public abstract <V> Optional<V> optionalTransform(Function<? super T, Optional<V>> function) {
    return function.apply(reference);
}

The implementation for Absent is unchanged from transform:

public abstract <V> Optional<V> optionalTransform(Function<? super T, Optional<V>> function) {
    checkNotNull(function);
    return Optional.absent();
}

It would also be nice if there were a way to handle methods that return null as opposed to Optional for working with legacy objects. Such a method would be like transform but simply call Optional.fromNullable on the result of the function.

I'm curious if anyone else has run into this annoyance and found nice workarounds (which don't involve writing your own Optional class). I'd also love to hear from the Guava team or be pointed to discussions related to the issue (I didn't find any in my searching).

like image 296
Yona Appletree Avatar asked May 31 '13 22:05

Yona Appletree


People also ask

Does optional chaining work on null?

Description. The optional chaining operator provides a way to simplify accessing values through connected objects when it's possible that a reference or function may be undefined or null .

Is optional chaining safe?

Optional chaining is a safe and concise way to perform access checks for nested object properties.

Why is optional better for null handling?

In a nutshell, the Optional class includes methods to explicitly deal with the cases where a value is present or absent. However, the advantage compared to null references is that the Optional class forces you to think about the case when the value is not present.

Does TypeScript support optional chaining?

Optional chaining is not a feature specific to TypeScript.


1 Answers

You are looking for some Monad, but Guava's Optional (as opposite to for example Scala's Option) is just a Functor.

What the hell is a Functor?!

Functor and Monad are a kind of box, a context that wraps some value. Functor containing some value of type A knows how to apply function A => B and put the result back into Functor. For example: get something out of Optional, transform, and wrap back into Optional. In functional programming languages such method is often named 'map'.

Mona.. what?

Monad is almost the same thing as Functor, except that it consumes function returning value wrapped in Monad (A => Monad, for example Int => Optional). This magic Monad's method is often called 'flatMap'.

Here you can find really awesome explanations for fundamental FP terms: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html

Functors & Monads are coming!

Optional from Java 8 can be classified as both Functor (http://docs.oracle.com/javase/8/docs/api/java/util/Optional.html#map-java.util.function.Function-) and Monad (http://docs.oracle.com/javase/8/docs/api/java/util/Optional.html#flatMap-java.util.function.Function-).

Nice mon(ad)olog, Marcin, but how can I solve my particular problem?

I'm currently working on a project that uses Java 6 and yesterday I write some helper class, called 'Optionals', which saved me a lot of time.

It provides some helper method, that allows me to turn Optional into Monads (flatMap).

Here is the code: https://gist.github.com/mkubala/046ae20946411f80ac52

Because my project's codebase still uses nulls as a return value, I introduced Optionals.lift(Function), which can be used to wrapping results into the Optional.

Why lifting result into Optional? To avoid situation when function passed into transform might return null and whole expression would return "present of null" (which by the way is not possible with Guava's Optional, because of this postcondition -> see line #71 of https://code.google.com/p/guava-libraries/source/browse/guava/src/com/google/common/base/Present.java?r=0823847e96b1d082e94f06327cf218e418fe2228#71).

Couple of examples

Let's assume that findEntity() returns an Optional and Entity.getDecimalField(..) may return BigDecimal or null:

Optional<BigDecimal> maybeDecimalValue = Optionals.flatMap(
    findEntity(),
    new Function<Entity, Optional<BigDecimal>> () {
        @Override 
        public Optional<BigDecimal> apply(Entity input) {
            return Optional.fromNullable(input.getDecimalField(..));
        }
    }
);

Yet another example, assuming that I already have some Function, which extracts decimal values from Entities, and may return nulls:

Function<Entity, Decimal> extractDecimal = .. // extracts decimal value or null
Optional<BigDecimal> maybeDecimalValue = Optionals.flatMap(
    findEntity(),
    Optionals.lift(extractDecimal)
);

And last, but not least - your use case as an example:

Optional<Publisher> maybePublisher = Optionals.flatMap(findBook(id), Optionals.lift(Book.getPublisher));

// Assuming that getPublishedBooks may return null..
Optional<List<Book>> maybePublishedBooks = Optionals.flatMap(maybePublisher, Optionals.lift(Publisher.getPublishedBooks));

// ..or simpler, in case when getPublishedBooks never returns null
Optional<List<Book>> maybePublishedBooks2 = maybePublisher.transform(Publisher.getPublishedBooks);

// as a one-liner:
Optionals.flatMap(maybePublisher, Optionals.lift(Publisher.getPublishedBooks)).transform(Publisher.getPublishedBooks);
like image 142
Marcin Kubala Avatar answered Sep 29 '22 05:09

Marcin Kubala