Optional Is a Law-breaking Monad but a Good Type
In most programming languages empty-or-not-empty types are well-behaved monads. (Yes, I used the M-word — don’t worry, no math.) This means their mechanics fulfill a couple of definitions and follow a number of laws that guarantee safe (de)composition of computations.
Optional
's methods fulfill these definitions but break the laws. Not without consequences...
Monad Definition
You need three things to define a monad — in Optional
's terms:
- The type
Optional<T>
itself. - The method
ofNullable(T)
that wraps a valueT
into anOptional<T>
. - The method
flatMap(Function<T, Optional<U>>)
that applies the given function to the value that is wrapped by theOptional
on which it is called.
There’s an alternative definition using map
instead of flatMap
, but it’s too long to fit here.
Monad Laws
Now it gets interesting — a monad has to fulfill three laws to be one of the cool kids. In Optional
's terms:
- For a
Function<T, Optional<U>> f
and a valuev
,f.apply(v)
must equalOptional.ofNullable(v).flatMap(f)
. This left identity guarantees it doesn't matter whether you apply a function directly or letOptional
do it. - Calling
flatMap(Optional::ofNullable)
returns anOptional
that equals the one you called it on. This right identity guarantees applying no-ops doesn't change anything. - For an
Optional<T> o
and two functionsFunction<T, Optional<U>> f
andFunction<U, Optional<V>> g
the results ofo.flatMap(f).flatMap(g)
ando.flatMap(v -> f.apply(v).flatMap(g))
must be equal. This associativity guarantees that it doesn't matter whether functions are flat-mapped individually or as a composition.
While Optional
holds up in most cases, it doesn't for a specific edge case. Have a look at flatMap
's implementation:
public <U> Optional<U> flatMap(Function<T, Optional<U>> f) {
if (!isPresent()) {
return empty();
} else {
return f.apply(this.value);
}
}
You can see that it doesn’t apply the function to an empty Optional
, which makes it easy to break left identity:
Function<Integer, Optional<String>> f =
i -> Optional.of(i == null ? "NaN" : i.toString());
// the following are not equal
Optional<String> containsNaN = f.apply(null);
Optional<String> isEmpty = Optional.ofNullable(null).flatMap(f);
That’s not great, but it’s even worse for map
. Here, associativity means that given an Optional<T> o
and two functions Function<T, U> f
and Function<U, V> g
the results of o.map(f).map(g)
and o.map(f.andThen(g))
must equal.
Function<Integer, Integer> f = i -> i == 0 ? null : i;
Function<Integer, String> g = i -> i == null ? "NaN" : i.toString();
// the following are not equal
Optional<String> containsNaN = Optional.of(0).map(f.andThen(g));
Optional<String> isEmpty = Optional.of(0).map(f).map(g);
So what?
The examples may seem contrived and the importance of the laws unclear, but the impact is real: In an Optional
chain you can't mechanically merge and split operations because that may change the code's behavior. That is unfortunate because proper monads let you ignore them when you want to focus on readability or domain logic.
But why is Optional
a broken monad? Because null
-safety is more important! To uphold the laws, an Optional
would have to be able to contain null
while being non-empty. And it would have to pass it to functions given to map
and flatMap
. Imagine, everything you did in map
and flatMap
had to check for null
! That Optional
would be a great monad, but provide zero null
-safety.
No, I'm happy we got the Optional
that we got.