Most functional programming languages offer a concept called Option or Maybe to deal with the presence or absence of a value, thus avoiding null
. Java 8 introduced java.util.Optional
, an implementation of the Maybe type for Java developers. Sadly, due to its flexibility, Optional
is often misused, be it because the developer does not understand its power, or be due to lack of background in functional programming.
In this post I want to highlight a common pattern of misusing Optional
and how to fix it.
Note that instead of java.util.Optional
, I will use the Vavr Option
instead. Vavr is a lightweight library that brings Scala-like features to Java 8 projects. It focuses on providing a great developer experience both through consistent APIs and extensive documentation. See this short overview of how and why optional can help you. Head over to http://vavr.io if you want to know more.
But, everything here applies to either implementation.
A real world example
I want to start with a typical example that we can use as a refactoring candidate.
Let's consider the following use case: Load a user using a repository. If we find a user we check if the address is set, and if so, we return the street of the address, otherwise the empty string.
Using null
s we write code similar to this:
User user = repo.findOne("id");
if (user != null) {
Address address = user.getAddress();
if (null != address) {
return address.getStreet();
}
else {
return "";
}
}
Urgs. This is what I call a Cascading Pile of Shame.
Fixing this is easy, just use Option
:
Option<User> opt = Option.of(user);
if (opt.isPresent()) {
Option<Address> address = Option.of(user.getAddress());
if (address.isPresent()) {
return address.get().getStreet();
}
else {
return "";
}
}
Right?
Wrong! Each time Option
is used like this, a microservice dies in production. This fix is basically the same as above. Same complexity, same _Cascading Pile of Shame.
Instead we use the map
operator.
Map - the Swiss army knife of functional programming
map
is your friend when using Option
. Think of Option
as a nice gift box with something in it.
Suppose you are a good programmer and wrote your code Test-First. You get a gift box with socks.
But who wants socks? You want a ball. So you map
a function to the gift box, that takes socks and transforms them to a ball. The result is then put into a new gift box. Your birthday is saved through the power of monads.
What if you are a bad coder and do not write unit tests at all? Well, then you won't get any nice socks. But map
still works fine:
If the gift box is empty, then map
won't even apply the function. So, basically it is "nothing from nothing".
Fixing things
So going back to the original problem, let's refactor this using Option
.
User user = repo.findOne("id");
if (user != null) {
Address address = user.getAddress();
if (null != address) {
return address.getStreet();
}
}
First of all, let findOne
return Option<User>
instead of null
:
Option<User> user = repo.findOne("id");
...
Since the user's address is optional (see what I did there ;) User#getAddress
should return Option<Address>
. This leads to the following code:
Option<User> user = repo.findOne("id");
user.flatMap(User::getAddress)
...
Why flatMap
...well, I'll leave that as an exercise. Just remember, that User#getAddress
return Option<Address>
and think about, what would happen if you used map
?
Now that we've got the Option<Address>
we can map
again:
Option<User> user = repo.findOne("id");
user.flatMap(User::getAddress)
.map(Address::getStreet)
...
Finally, we only need to decide what to do if everything else fails:
Option<User> user = repo.findOne("id");
user.flatMap(User::getAddress)
.map(Address::getStreet)
.getOrElse("");
Which leaves us with the final version:
repo.findOne("id")
.flatMap(User::getAddress)
.map(Address::getStreet)
.getOrElse("");
If you read it from top to bottom, this is as literal as it gets.
repo.findOne("id") // Find a user
.flatMap(User::getAddress) // if an address is available
.map(Address::getStreet) // fetch the addresses street
.getOrElse(""); // otherwise use the empty string
Summary
I hope this short post illustrates the usefulness of Vavr and its Option
abstraction. If you remember one thing only, then please let it be Do not use Option#isPresent
or Option#get
, the map
is your friend.
Vavr as a library offers many amazing extensions for object-functional programming in Java, even for brownfield projects. You can leverage its utilities where they make sense and need not migrate to Scala or similar platforms to reap at least some benefits of functional programming.
Of course, this is all syntactic sugar. But as any good library, Vavr fixes things, the core JDK cannot take care of so easily without breaking a lot of code.
Future posts will cover its other amazing features like pattern matching, property based testing, collections and other functional enhancements.
Top comments (3)
I find the 'exercise for the user' quite confusing since no clear signature for those methods are given and the return type changes between the examples.
Anyway, if anyone is wondering,
flatMap
is used becausegetAddress
returns anOption<Address>
in one of the examples.map
is used instead with 'getStreet' because it returns a plainString
.Good point, thank you. I have added a small extra sentence, that hopefully reduced the confusion.
To add to your answer:
flatMap
expects a mapper that returns anOption
.map
on the other hand wraps the result of the applied mapper function into eithersome
ornone
.In short:
map
creates a new "Gift Box" andflatMap
reuses the "Gift Box"."Each time Option is used like this, a microservice dies in production."
😂😂
Nice article!