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...
You need three things to define a monad—in Optional
’s terms:
The type Optional<T>
itself
The method ofNullable(T)
that wraps a value T
into an Optional<T>
The method flatMap(Function<T, Optional<U>>
) that applies the given function to the value that is wrapped by the Optional
on which it is called
There’s an alternative definition using map
instead of flatMap
, but it’s too long to fit here.
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 value v
, f.apply(v)
must equal Optional.ofNullable(v).flatMap(f)
. This left identity guarantees it doesn’t matter whether you apply a function directly or let Optional
do it.
Calling flatMap(Optional::ofNullable)
returns an Optional
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 functions Function<T, Optional<U>> f
and Function<U, Optional<V>> g
, the results of o.flatMap(f).flatMap(g)
and o.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 be 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);
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 nonempty. And it would have to pass it to functions given to map
and flatMap
. Imagine if 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.