Geek Culture

A new tech publication by Start it up (https://medium.com/swlh).

Is java.util.Optional a Monad?

alex_ber
5 min readOct 13, 2019

--

Let’s start from the definition of what is Monad:

“We can define a monad as:

  • A parameterized type M<T>: in Java terms, public class M<T>.
  • A unit function, which is a factory function to make a monad out of an element: public <T> M<T> unit(T element).
  • A bind operation, a method that takes a monad as well as a function mapping an element to a monad, and returns the result of applying that function to the value wrapped in the monad:
public static <T, U> M<U> bind(M<T> monad, Function<T, M<U>> f) {
return f.apply(monad.wrappedValue());
}
  • Is that all there is to know about monads? Not really, but that is enough for now.

Optional per se qualifies as a monad, despite some resistence in the Java 8 library team. Let’s see how it fits the 3 properties above:

  • M<T> is Optional<T>.
  • The unit function is Optional.ofNullable.
  • The bind operation is Optional.flatMap.

So it would seem that Optional is indeed a monad, right? Not so fast.

Any class, to truly be a monad, is required to obey 3 laws:

  1. Left identity, applying the unit function to a value and then binding the resulting monad to function f is the same as calling f on the same value: let f be a function returning a monad, then bind(unit(value), f) === f(value).
  2. Right identity, binding the unit function to a monad doesn’t change the monad: let m be a monadic value (an instance of M<T>), then bind(m, unit) === m.
  3. Associativity, if we have a chain of monadic function applications, it doesn’t matter how they are nested: bind(bind(m, f), g) === bind(m, x -> g(f(x))).

Both left and right identity guarantee that applying a monad to a value will just wrap it: the value won’t change nor monad will be altered. The last law guarantees that monadic composition is associative. All laws together make code more resilient, preventing counter-intuitive program behaviour that depends on how and when you create a monad and how and in which order you compose functions that you will use to map a monad.

…: Does Optional<T> have these properties?

Let’s find out by checking property 1, Left Identity: [no, conter-example]

Function<Integer, Optional<Integer>> f = x -> {
if (x == null) {
x = -1;
} else if (x == 2) {
x = null;
} else {
x = x + 1;
}
return Optional.ofNullable(x);
};
// true, Optional[2] === Optional[2]
Optional.of(1).flatMap(f).equals(f.apply(1));
// true, Optional.empty === Optional.empty
Optional.of(2).flatMap(f).equals(f.apply(2));

This works both for empty and non-empty results. What about feeding both sides with null?

// false [counter-example!]Optional.ofNullable((Integer) null).flatMap(f)
.equals(f.apply(null));

This is somehow unexpected. Let’s see what happens:

// prints "Optional.empty"
System.out.println(Optional.ofNullable((Integer) null).flatMap(f));
// prints "Optional[-1]"
System.out.println(f.apply(null));

So, all in all, is Optional a monad or not? Strictly speaking it’s not a well-behaving monad, since it doesn’t abide by the monad laws. However, since it does satisfy the definition of a monad, it could be considered one, although one with some buggy methods.

If you think we got out of luck with flatMap, wait to see what happens with map.

When we are using Optional.map, null is also mapped into Optional.empty. Suppose we map again the result of the first mapping into another function. Then that second function won’t be called at all when the first one returns null. If, instead, we map the initial Optional into the composition of the two functions, the result would be quite different. [bold is mine] Check out this example to clarify:

Function<Integer, Integer> f = x -> (x % 2 == 0) ? null : x;
Function<Integer, String > g = y -> y == null ? "no value"
: y.toString();
// A value that f maps to null - this breaks .map
Optional<Integer> opt = Optional.of(2);
opt.map(f).map(g); // Optional.emptyopt.map(f.andThen(g)); // "no value"

By composing the functions f and g (using the handy Function::andThen) we get a different result than we got when applying them one by one [bold is mine]. An even more obvious example is when the first function returns null and the second throws a NullPointerException if the argument is null. Then, the repeated map works fine because the second method is never called but the composition throws the exception.

So, Optional::map breaks the associativity law. This is even worse than flatMap breaking the left identity law…

What’s The Catch with Optional?

The problem is that by design non-empty Optionals can’t hold null. You might legitimately object it is designed to get rid of null, after all: And in fact Optional.of(null) will throw a NullPointerException. Of course null values are still common, so ofNullable was introduced to keep us from repeating the same if-null-then-empty-else-of check all over our code. However – and here is the essence of all evil – Optional.ofNullable(null) is translated to Optional.empty.

The net result is that, as shown above, the following two situations can lead to different results:

  • Applying a function before wrapping a value into Optional;
  • Wrapping the value into an Optional first and then mapping it into the same function.

This is as bad as it sounds: it means that the order in which we apply functions matters. When we use map, as we saw, it gets even worse, because we lose associativity invariance as well and even the way functions are composed matters.

In turn, these issues make adding bugs during refactoring not just possible, but even frighteningly easy.

Practical Implications

Besides theoretical disputes on the nature of Optional, there are plenty of practical consequences of the fact that Optional::map and Optional::flatMap break the monad laws. This in turn prevents us from freely applying function composition, as we were supposed to have the same result if we apply two functions one after the other, or their composition directly.

It means that we can no longer refactor our code freely and be sure the result won’t change: Dire consequences might pop up not just in your code base, but — even worse — in your clients’ code. Before restructuring your code, you would need to know if the functions used anywhere in everybody’s code handle null or not, otherwise you might introduce bugs.

© https://www.sitepoint.com/how-optional-breaks-the-monad-laws-and-why-it-matters/

Feel free to consult the link above for the real-world example. There are also some more parts that was skipped.

--

--

alex_ber
alex_ber

Written by alex_ber

Senior Software Engineer at Pursway

No responses yet