Chapter 63. Optional Is a Lawbreaking Monad but a Good Type

Nicolai Parlog

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 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);

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 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.193.129