Optional Is a Law-breaking Monad but a Good Type

Nicolai Parlog
97 Things
Published in
3 min readJul 18, 2019

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:

  1. The type Optional<T> itself.
  2. The method ofNullable(T) that wraps a value T into an Optional<T>.
  3. 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.

Monad Laws

Now it gets interesting — a monad has to fulfill three laws to be one of the cool kids. In Optional's terms:

  1. 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.
  2. 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.
  3. 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 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.

--

--

Nicolai Parlog
97 Things

Nicolai is a #Java enthusiast with a passion for learning and sharing — in posts & books; in videos & streams; at conferences & in courses. https://nipafx.dev