Yona Appletree
Yona Appletree

Reputation: 9142

Null-safe method chaining with Optional

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).

Upvotes: 15

Views: 5836

Answers (2)

Marcin Kubala
Marcin Kubala

Reputation: 591

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);

Upvotes: 12

siledh
siledh

Reputation: 3378

You probably figured that out, but you could add .or(Optional.absent) after every transformation that returns Optional (in your case after .transform(Book.getPublisher) reducing Optional<Optional<T>> to Optional<T>:

Optional<List<Book>> publishedBooks = findBook(id).transform(Book.getPublisher).
        or(Optional.absent()).transform(Publisher.getPublishedBooks);

Unfortunately, the type of Optional.absent cannot be inferred here, so the code actually becomes:

Optional<List<Book>> publishedBooks = book.transform(Book.getPublisher).
        or(Optional.<Publisher> absent()).transform(Publisher.getPublishedBoooks);

Not too convenient but there doesn't seem to be any other way.

Upvotes: 0

Related Questions