What's Wrong in Java 8, Part IV: Monads
Further exploring what's wrong with Java 8, we turn our discussion to monads.
Join the DZone community and get the full member experience.
Join For FreeMonads are central to Functional programming. They are generally totally ignored in imperative programming. They are even often feared by imperative programmers. Most Java programmers either do not know what a monad is, or would raise strong protest if monads were to be introduced in Java.
But, although this has not been much advertised, Java 8 brings monads.
In a recent post, (http://java.dzone.com/articles/java-8-optional-whats-point) Hugues Johnson asks about Java 8 Optional
: Whats the point?
The example he gives is close to the following:
String getString() {
//method returning a String or null, such as get on a Map<String, String>
}
Option<String>optionalString=Optional.ofNullable(getString());
optionalString.ifPresent(System.out::toString);
He then asks what the point of using Optional
since we can do the same without it:
String getString() {
//method returning a String or null, such as get on a Map<String, String>
}
String string = getString();
if (string != null) System.out.println(string);
This is missing the point. The important point is that Optional
is a monad and is supposed to be used as such.
What is a monad?
Without entering the details of Category theory, a monad is a very simple, yet extremely powerful thing. A monad a set of three things:
- a parameterized type M<T>
- a “unit” function T -> M<T>
- a “bind” operation: M<T> bind T -> M<U> = M<U>
These may seems over complicated, but it is really simple considering an example, such as the Optional
monad:
- Parameterized type:
Optional<T>
- unit:
Optional.of()
- bind:
Optional.flatMap()
Before showing how to use this monad, we must be sure of one thing: we may not know the exact intention of Java 8 designers, but they surely did not create this at random. If they created monads, they surely did it on purpose. And they probably did not use the name “monad” to avoid frightening imperative programmers.
How to use the Optional monad
The Optional
monad is meant to allow composing functions that may or may not return a value, when the absence of value is not an error. The most common example is searching for a key in a map. Suppose we have the following:
Person person = personMap.get("Name");
process(person.getAddress().getCity());
Here, we are searching a person by its name in a map, then we get the person address, then the city in the address, and we pass the result to the process
method. Lot of things may happen:
- The
"Name"
key may not be present in the map resulting inperson
beingnull
. - If the key is present,
person.getAdress()
may returnnull
. - If the
address
is notnull
, thecity
may benull
. - Eventually all may run fine, with nothing being
null
.
(We will not consider the case were the null
value is bind to the “Name”
key in the map.)
In the first three cases, we get an NPE. To handle this, we must do the following:
Person person = personMap.get("Name");
if (person != null) {
Adress address = person.getAddress();
if (address != null) {
City city = address.getCity();
if (city != null) {
process(city)
}
}
}
How can we use Optional
in this code to clear up this mess? Well, we can't. To use Optional
, we have first to modify the Map
, Person
, and Address
classes so that all their methods return an Optional
. Then, we can change our code to:
Optional<Person> person = personMap.get("Name");
if (person.isPresent()) {
Optional<Adress> address = person.getAddress();
if (address.isPresent()) {
Optiona<City> city = address.getCity();
if (city.isPresent()) {
process(city)
}
}
}
But this is NOT how Optional
is intended to be used. Optional
is a monad and is intended to be use as such:
personMap.find("Name")
.flatMap(Person::getAddress)
.flatMap(Address::getCity)
.ifPresent(ThisClass::process);
This implies that we use a modified version of Map:
public static class Map<T, U> extends HashMap<T, U> {
public Optional<U> find(T key) {
return Optional.ofNullable(super.get(key));
}
}
and that methods getAddress
and getCity
return Optional<Address>
and Optiona<City>
.
Note: we have used method references in this example to clarify the code, and one could argue that method references are not functions. In fact, this is only syntactic sugar over:
personMap.find("Name")
.flatMap(x -> x.getAddress())
.flatMap(x -> x.getCity())
.ifPresent(() -> process(x));
Here, x -> x.getAddress()
is a function of type T -> M<U>.
Note 2: remark that ifPresent
is exactly the same as forEach
for collections and streams. It should have been called forEach
although there is at most one element. This should have make clearer the relation between the Optional
monad and the List
monad. Unfortunately, List
is not a monad in Java 8, but it can be transformed easily to a Stream
.
Of course, some of these methods may always return a value. For example, given the following class:
public static class Address {
public final City city;
public Address(City city) {
this.city = city;
}
public City getCity() {
return city;
}
}
we may change our code to:
personMap.find("Name")
.flatMap(Person::getAddress)
.map(Address::getCity)
.ifPresent(ThisClass::process);
This is how Optional
is meant to be used.
The (missing) Try monad
So Java 8 has monads. Stream
is also a monad. We should use monads for what they were intended, and those who fear them may still ignore them and continue to deal with NPE.
Then all is good? Nope. The main problem is that there are many things missing in order to promote the use of monads. In this example, we can see that the get
method in Map
should have been updated, as is the case for all methods that would possibly return null
. As it is no possible to break backward compatibility, they should have been deprecated and new methods should have been added like the find
method in the above map. (I will not speak about method taking null
as an argument, since these methods should never have existed.)
Another major concern is that the Optional
monad is only good for cases where the absence of value is not an error. In our case, if all methods should return a value or throw an exception, we would be in trouble because we have no monad for this.
Here is what we could do in imperative programming:
Person person = personMap.get("Name");
if (person != null) {
Adress address = person.getAddress();
if (address != null) {
City city = address.getCity();
if (city != null) {
process(city)
} else {
throw new IllegalStateException("Address as no city");
}
} else {
throw new IllegalStateException("Person has no address");
}
} else {
throw new IllegalStateException("Name not found in map");
}
Note that throwing exceptions like this is a modern form of goto
, with the difference that we do not know where we are going.
To apply the same programming style we have used with Optional
, we need another monad that is often called Try
. But Java 8 does not have it. We may write it ourselves, which is not very difficult:
public abstract class Try<V> {
private Try() {
}
public abstract Boolean isSuccess();
public abstract Boolean isFailure();
public abstract void throwException();
public static <V> Try<V> failure(String message) {
return new Failure<>(message);
}
public static <V> Try<V> failure(String message, Exception e) {
return new Failure<>(message, e);
}
public static <V> Try<V> failure(Exception e) {
return new Failure<>(e);
}
public static <V> Try<V> success(V value) {
return new Success<>(value);
}
private static class Failure<V> extends Try<V> {
private RuntimeException exception;
public Failure(String message) {
super();
this.exception = new IllegalStateException(message);
}
public Failure(String message, Exception e) {
super();
this.exception = new IllegalStateException(message, e);
}
public Failure(Exception e) {
super();
this.exception = new IllegalStateException(e);
}
@Override
public Boolean isSuccess() {
return false;
}
@Override
public Boolean isFailure() {
return true;
}
@Override
public void throwException() {
throw this.exception;
}
}
private static class Success<V> extends Try<V> {
private V value;
public Success(V value) {
super();
this.value = value;
}
@Override
public Boolean isSuccess() {
return true;
}
@Override
public Boolean isFailure() {
return false;
}
@Override
public void throwException() {
//log.error("Method throwException() called on a Success instance");
}
}
// various method such as map an flatMap
}
Such a class may be use to replace Optional
. The main difference is that if at some stage in the program some component returns a Failure
instead of a Success
, it will still compose with the next method call. For example, given that Map.find()
, Person.getAddress()
and Address.getCity()
all return a Try<Something>
our previous example may be rewritten as:
personMap.find("Name")
.flatMap(Person::getAddress)
.flatMap(Address::getCity)
.ifPresent(This.class::process);
Yes, this is exactly the same code as what we wrote with Optional
. The differences are in the classes used, for example:
public static class Map<T, U> extends HashMap<T, U> {
public Try<U> find(T key) {
U value = super.get(key);
if (value == null) {
return Try.failure("Key " + key + " not found in map");
}
else {
return Try.success(value);
}
}
}
As Java does not allow overriding methods which differ only by the return type, we would have to choose different names for method returning Optional
and Try
. But this is not even necessary, since Try
may be used to replace Optional
. The only difference is that if we want to process the exception, we can have special methods in the Try
class such as:
public void ifPresent(Consumer c) {
if (isSuccess()) {
c.accept(successValue());
}
}
public void ifPresentOrThrow(Consumer<V> c) {
if (isSuccess()) {
c.accept(successValue());
} else {
throw ((Failure<V>) this).exception;
}
}
public Try<RuntimeException> ifPresentOrFail(Consumer<V> c) {
if (isSuccess()) {
c.accept(successValue());
return failure("Failed to fail!");
} else {
return success(failureValue());
}
}
This gives the user (the business programmer, as opposed to the API designer) the choice of what to do. He can use ifPresent
to obtain with Try
the same result as with Optional
and ignore any exception, or he can use ifPresentOrThrow
to throw the exception if there is one, or he can use ifPresentOrFail
if he wants to handle the exception in any other way, as in the following example:
personMap.find("Name")
.flatMap(Person::getAddress)
.flatMap(Address::getCity)
.ifPresentOrFail(TryTest::process)
.ifPresent(e -> Logger.getGlobal().info(e.getMessage()));
Note that the exception we get at the end of the chain may have occurred in any of the methods. It is simply transmitted from one function to the next. So the API designer does not have to care about what to do with the exception. The result is the same as if the exception had been thrown and caught by the user, without the need of a try/catch block, and without the risk of forgetting to catch it in case of an unchecked exception.
Other useful monads
Many other monads may be very useful. We may use monads to handle randomness (function that usually return a changing value, such as date and random generators). We may use monads to handle functions returning multiple values. List
is not a monad in Java, but we can easily create it, or we can get a Stream
from a list, and Stream
is a monad. We may also use monads to deal with future values, and even for primitives wrappers.
In Java 8, the Collection.stream()
method may be used to transform a collection into a monad.
So what is wrong?
Since it is so simple to write our own monads, is there something wrong? In fact there are at least three.
The first one is that although we can create the missing monads, we will not be able to use them in a public API, because each API would have a different, incompatible implementation. We need a standard implementation we can share.
The second is that Java API should have been retrofitted to use these monads. It should have been done at least for Optional
.
Another recurrent problem is due to primitives. Optional
won't work with primitives, so there are special versions for int
, double
and long
(OptionalInt
, OptionalDouble
and OptionalLong
). We really need value types! (but this might be coming: see http://www.dzone.com/links/r/brian_goetz_value_types_big_on_the_agenda_for_fut.html)
Previous articles
What's Wrong in Java 8, Part I: Currying vs Closures
What's Wrong in Java 8, Part II: Functions & Primitives
What's Wrong in Java 8, Part III: Streams & Parallel Streams
Opinions expressed by DZone contributors are their own.
Comments