- Functional programming magic
- Why functional programming?
- You don't need to check its presence
Every Java programmer knows Optional
type. If someone doesn't, shame on her/him. In this post I'll show you the power of Optional
and will explain in details where does this power comes from.
What is Optional
?
First of all, Optional
is a Monad. There will be more about Monads in another post but for the sake of this article I'm highlighting some
characteristics.
-
Every Monad has factory method:
Optional.ofNullable()
orOptional.of()
-
Every Monad is a parameterized container:
Optional<T>
-
Every Monad has a mapping method taking function accepting monad's type and returning Monad:
Optional<U> flatMap(Function<? super T, Optional<U>> mapper)
- Monads are immutable
-
Finally and last but not least Monad is a Monad only if it obeys three laws: Left identity, Right
identity, Associativity. You must take it on faith now,
Optional
obeys all of them.
Optional
is a Monad indicating whether it holds some value or is empty. It is very useful replacement of null
and we should
avoid nulls
at all costs because null
is a huge code smell. There is not enough place here to express how smelly it is but we
can Google it: java null criticism,
Why you are using Optional
wrong?
Is it happening to you more often than very rarely to write code like this?
If it does, please stop doing this, you are hurting all Monads. The fact that Optional.isPresent()
is available doesn't mean that we
should use it except exceptional situations. If we would like to follow monadic conventions, above code should look like this:
These code snippets lengths difference is striking, isn't it? The calculateSalary
method signature is changed too, the return type is Optional<BigDecimal>
now. It might seem to be minor difference but is quite significant. We don't have to document internal implementation behavior of returning null
in case of not found employee, we expressed it by method's signature. We inverted the control making caller's code to decide what to do in such case,
whether pass some other Optional
further or maybe throw an exception. We just don't care here and this is good.
Don't consume Optional
to early, let it flow through your code for the sake of clarity and safety.
Map and filter it
Next case are the heavily underused map
and filter
methods. Let's say we have string values stored in the map and expose it via
some service class. Additional features provided by this class is the validation of the keys used to obtain values and some method used to decorate
returned values. This is traditional example implementation.
This series of if
statements can be easily replaced with Optional
.
We can go further and get rid of blank key check before Optional
creation.
We are allowed to use cache::get
in line 10 because Optional.map()
expects Function<? super String,? extends String>
there. It means function accepting any superclass of String
and
returning any subclass of String
. Our cache.get()
method reference with signature Function<Object,String>
fits there.
I personally find it much more convenient and descriptive to apply filters and transformations in this way. If you don't, maybe next example will be more convincing.
Our task it to write method parsing text provided by user to percent format. To make things more complicated, users are from country using comma as decimal separator. This is simple implementation.
And this is equivalent implementation using Optional
.
One might say that this is semantically the same code, only syntax vary. Well, that's true, but there is one difference you might not spot. In first
example we did several intermediate assignments in lines 7, 9 and 11 to increase readability. It required additional mental work to think about clean variables names. When we perform
series of transformations with Optional.map()
we can avoid this job.
Additional thing to point out is the weird exception thrown in line 19. This is dead code actually so it
doesn't matter what exception is declared to be thrown, it will never be thrown anyway. It will never be thrown because any problematic value will cause NumberFormatException
in line 11. It doesn't look good and would trigger questions and discussions. The reason is impure functions
usage and bad API design. The impure function is BigDecimal
constructor. It throws NumberFormatException
in case of non number
string provided as an argument. Pure functions always return value and never throw exceptions. In previous examples we also saw the rule applied: Don't
consume Optional
to early
. Let's change the parser code to return Optional
and use pure functions.
This time we use pure createBigDecimal(String str)
function to create Optional<BigDecimal>
from String
. It
will return Optional.empty()
in case of invalid number representation. To handle nested Optional
mapping, we use Optional.flatMap()
in line 19, otherwise the type returned by expression would be Optional<Optional<String>>
.
One thing to notice is that we don't know the reason why our parsePercent
function returned empty Optional
: because provided
argument is null
, blank string or invalid number representation? The calling code knows only whether there is some value or there is now
value returned. There is handy way of handling such cases with ... of course another Monad named Try
. There will be another post about Try
Monad but here below is the same code using Try
from Vavr library.
The Try
and Optional
based code similarities are really striking, so much that it's easy to overlook the differences by just
reading the code. Furtunately, the compiler will spot the difference. The caller code will not differ much as well. Used Try
implementation
provides bunch of methods very similar to those supplied by Optional
, like: orElse
, getOrElseGet
or getOrElseThrow
.
We can even convert the Try
to Optional
with toJavaOptional()
method if we don't care about error details.
This is the true power of Monads: we use all of them in the same way. If we know how to use Optional
, we know how to use other Monads.
Handling missing nested values
To let you feel what I mean by deep nested missing values, I'd like to recall you old Java XML W3C DOM API. I find it most tedious to work with,
because methods it provides, very often return null
values. Additionally, XML's Document Object Model is the tree of nodes that can be really
deep.
Let's imagine that we have users database stored in XML file like this one:
As we can see, some users have address specified and some do not. To get user's street from address, we have to access it via quite long path:
-
find node named
user
-
check whether child node
id
has expected text content -
get
location
child node -
get
address
child node iflocation
is present -
get
street
child node ifaddress
is present
I'll let you you imagine how boring and error prone native DOM API use would be. To mitigate this, we created several functions operating on org.w3c.dom.Node
and using Optional
as return value heavily.
First, we need the easy way to iterate over child nodes. Unfortunately, NodeList
returned by Node.getChildNodes()
isn't Iterable
.
Lets' create our own implementation.
If we have Iterator
, we can use Stream
created from it. This is function creating Stream
from NodeList
.
Next, we need some predicates to check various node properties.
As we can see, there is Optional
used all the time. This is due to the fact, that Node
methods can return null
depending on Node
type. For example, getNodeName()
or getAttributes()
methods called on text node always return null
.
We create Optional
using of()
factory method because we don't accept null
in predicates. In this way, we will fail
fast with NullPointerException
.
Then we need functions finding nodes. Here they are.
Finally, if we have node finding functions, we can easily create additional useful predicate checking whether given Node
has child matching
given condition.
Let's see our toys in action.
This code is totally safe (unless we left some bugs) and way easier to write and comprehend. We created custom Node
Predicate
from primitive ones for convenience and used in line 3.
This predicate checks, whether given node name is user
and has child node named id
with text content equal given id
argument.
Below test code is identical to previous on, only assertion differs because we don't expect Mary Jane (having id 2) to have address specified at all.
What if we ask about non existing user? No problem.
There is no user with id 3 and the test doesn't fail.
And what about using attributes not present in our XML file? No issues at all.
There is no user
node with attribute name
and the test passes.
As we can see, by writing less then 100 lines of code using Optional
heavily we made our life with XML DOM API easier and safer.
Eager and lazy alternative value
One of the frequent usages of Optional
is to get alternative value in case of empty Optional
. We do this via orElse(T
other)
or orElseGet(Supplier<? extends T> other)
method. It's common mistake to use orElse
while orElseGet
should be used. The difference between these methods is that orElseGet
takes Supplier
, so actual alternative value is evaluated
lazy, only if is needed meaning when Optional
is empty. Java evaluates methods arguments before method body execution, we call it eager
evaluation. Improper selection of alternative value provider may result in unexpected behaviors, like in below example.
Let's say, we have repository interface and implementation backed up by some persistence technology. We express missing lookup by primary identifier by
returning Optional
from appropriate method.
getDefault
in line 5 returns the Entity
by hardcoded id
value and can be
used if the one we were looked for was not found. It also prints debug message for presentation purpose. Here below is simple test showing the difference
between eager and lazy evaluation of Optional
alternative value.
orElseGet
alternative value provider works in this way because it expects Supplier
implementation. Supplier
is the
functional interface with single abstract method T get()
. This is all we need to obtain default value, just call get()
and
return its result. This is exactly what orElseGet
method does. Without calling Supplier.get()
nothing happens. On the other
side, orElse
expects default value which is evaluated before execution of our code using Optional
. If we want to return simple
default value, like BigDecimal.ZERO
, Optional.orElse()
is perfectly legitimate.
Whenever additional operations are needed to provide default value in case of empty Optional
, use orElseGet
. If the default
value is just simple object or constant, use orElse
.
Anatomy of Optional
If you think that behind Optional
's powers stays some sophisticated code ... you are wrong. The concept and implementation is actually very
easy. The best way to see what is happening under the hood is to implement our version of Optional
. Let's name it Maybe
to
distinguish it from Java's Optional
and Scala's Option
.
Our Maybe
is the generic container, so it has to be parameterized.
If we think about very nature of Maybe
it's just either something or nothing. We can easily design it as abstract class with only
two implementations: Just
and Nothing
. We don't want to allow any other implementations to exist, so let's make Maybe
,
Just
and Nothing
constructors private. Just
and Nothing
will be static and final inner classes of Maybe
.
Just
should hold some value, let's add the value placeholder to it.
There is only one and only one Nothing
, we can create singleton for it.
If you wonder why NOTHING
singleton is of unbounded wildcard type, the answer is simple: Nothing
holds no value, it's type is
meaningless and can be discarded.
Next, we need to have the ways to create Just
and Nothing
instances. We can't just construct them, because their constructors
are private. Let's write appropriate factory methods in Maybe
.
By factoring Nothing
we always return our singleton instance NOTHING
. We do cast it to Maybe<A>
to avoid
unnecessary unchecked cast warnings. Remember? Nothing
holds no value and has no type actually, it's safe to cast it to Maybe
of
any A
. Just
on the other hand must hold the value, null
is not acceptable, hence we validate just
factory method argument against null
references in line 7.
We would like to create Maybe
without knowing if it's nothing or something too. Let's craft the method doing so.
This is what we have so far.
We may have a value in Just
but we don't know how to check it. Let's add Maybe.isPresent()
abstract method and implementations.
Obviously, implementation will be simple but completely different.
This is Just
:
And this is Nothing
:
Let's quickly go through value accessors declaration in Maybe
, they are not very interesting anyway.
These are Just
implementations:
And these are taken from Nothing
:
We do completely opposite things in Just
and Nothing
respectively. This kind of symmetry is often present in functional
programming where Monads were born.
Finally the most important methods: flatMap
and map
. If you wonder why flatMap
is in the first place ... the reason
is simple, map
can be implemented using flatMap
.
Let's start with map
because it's easier to comprehend. Its signature is as follows: Maybe<B>
map(Function<? super A,? extends B> f)
. f
argument is named after Function
. What this method does is: it takes
given mapping function and returns new Maybe
containing result of this function applied to Maybe
value
. Nothing
implementation is simple, there is no value to apply given function to, we have only one choice, return Nothing
.
Just.map()
implementation is very straightforward too:
flatMap
behavior is harder to explain and easier to write: take Maybe
with some value in it and apply mapping function
returning other Maybe
to original Maybe
value.
Nothing.flatMap()
returns Nothing
, no surprises here.
Just.flatMap()
simply returns the result of given function applied to wrapped value.
flatMap
is the feature making Maybe
a Monad, actually map
is redundant because can be implemented using flatMap
.
It is usually implemented separately for performance reasons.
mapViaFlatMap
is a method implemented in our abstract Maybe
class. Presented above code is very functional. We can read is as: take
f
function, compose it with Maybe::maybe
, the composed function is maybe(f(v))
with signature Function<? super A,Maybe<B>>
, and pass it to flatMap
.
We can also implement it using lambda expression if we find it easier to grasp:
If you don't know what function composition is, you may find my post Functional programming magic quite
helpful. If you would like to see full Maybe
source, click here. You will find side
effects performing ifPresentOrElse
method there.
That's it, about 100 lines of code and we have fully functional Optional
replacement. We reinvented the wheel, there are plenty
implementations available but our understanding of Optional
is much better thanks to this exercise.
Summary
We know how to use Optional
. We know how simple and easy to implement concept it is. This is always like this: great power lies in
simplicity. I personally can't imagine my life without some kind of Optional
in my programming toolkit and I hope that after reading my post
you will not be able to do so either.