Monads in Java


Published: 2016-08-21
Updated: 2016-08-26
Web: https://fritzthecat-blog.blogspot.com/2016/08/monads-in-java.html


All the Internet agrees: it's quite hard to explain what a monad is. From reading all their attempts I finally could understand - that it's hard to explain :-)

This Blog is my attempt to explain what a monad is. I am a software developer, not a mathematician. I apply some mechanism, and if it is useful, I keep it. Monads, in context of multi-processor computers, are a useful mechanism.

You can find good examples for monad usages in the Java Lambda Tutorial. For understanding lambdas, read also my latest Blog.

What Is a Monad?

Expressed in object-oriented terms, a monad is the decoupling of object and method (as "function").
You do not call someObject.someMethod() directly any more, you tell the monad to do that.
This is beyond object-oriented thinking.

Why Monads?

Monads can

  1. help to avoid method-calls on objects that are null, and
  2. provide implicit loops, redistributable onto multiple processors.

Moreover they enable us to program in a Fluent Interface style. Other authors call this "pipeline", or "programmed semicolons".

Null Object

Ever heard about the "Billion Dollar Bug"? In old times I saw "segmentation violation" when the application crashed, nowadays in Java it is NullPointerException, and it doesn't crash any more. I am talking about calling a method on an object that is null, meaning it does not exist. Robot stepping into the void.

What's wrong when that happens? Is it developers fault? Developers write source code, hopefully checked by a compiler. Is it data fault? Data are known at runtime only, and thus can not be checked by any compiler. The source code needs to avoid data faults. So it is developer fault anyway, although there are discussions whether this really is a Billion-Dollar Bug.

The Java Optional monad gives us relief. This is a new class in java.util. Kind of null-object pattern.

Collections

Modern hardware architecture with multiple CPUs makes it necessary to rewrite our software to effectively use the hardware. The essential thing is to perform loops in parallel threads. Thus we need a new kind of loops where we do not write for, for-in, while, do-while statements explicitly any more. Only a wrapped, implicit loop can be redistributed onto multiple threads automatically.

Instead of writing


while (iterator.hasNext())
processElement(iterator.next())

we would need a "programmable" loop-method like


collection.forEach(element -> processElement(element))

The Java Stream monad gives us relief. This is a new interface in java.util.stream, supported by Java's collection-framework.

How Do Monads Work?

A monad is a wrapper over a given object. That given object also could be a collection of objects.

Wrapping 0-1 Object

In Java, there is always a factory for monad objects. This is called the monad's "unit":

Person person = ....; // could be null
Optional<Person> optionalPerson = Optional.ofNullable(person);

Optional is a class that holds a reference to a wrapped object or null. Optional.ofNullable() and Optional.of() are static factories. After the object was wrapped into its monad, operations on it should be done through that monad. This is then called the monad's "bind":

Optional<Address> optionalAddress = optionalPerson.map(person -> person.getAddress());

The return of map() would be a new monad, and you can call further functions on it. Here we get out an Optional wrapping the person's address, or Optional.EMPTY when one of person or address has been null. Mind that this is not a good strategy when address is a required field, the application should detect such errors.

A monad could provide different bindings. Stereotype bindings, seen on lots of monads, are:

So let's navigate through Person down to city without NullPointerException (see also source code on bottom):

Person testPerson = ...; // may be null!
String city = Optional.ofNullable(testPerson)
.map(person -> person.getAddress())
.map(address -> address.getCity())
.orElse("No city!");

The orElse() method returns the wrapped object in case it is not empty, else it returns the given default value "No city!".

The same without monad would look like this:

Person testPerson = ...; // may be null!
final String DEFAULT_CITY = "No city";
String city = (testPerson == null) ? DEFAULT_CITY :
(testPerson.getAddress() == null) ? DEFAULT_CITY :
(testPerson.getAddress().getCity() == null) ? DEFAULT_CITY :
testPerson.getAddress().getCity();

The monad variant wins with 10 points advance (of 100:-)

map() versus flatMap()

While map() produces a new monad to return, flatMap() requires the given function to do this. Further flatMap() does NOT allow null to be returned from the given function. Here are their implementations from JDK class Optional:

    public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if ( ! isPresent() )
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}

public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if ( ! isPresent() )
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}

A little code-duplication, but the first and last lines are different:-)

Wrapping 0-n Objects

Collections can be processed in parallel by wrapping them into monads. The interface Collection provides factories as "default methods", directly inside the interface (this is a new Java 8 feature), to create a Stream monad.

Collection<Person> persons = getPersons(); // must not return null!
Stream personStream = persons.stream();

The stream() method is the factory used here. The resulting monad provides lots of bindings you need for list processing.

String [] cities = personStream
.filter(person -> person.getName().startsWith("J"))
.sorted((person1, person2) -> person1.getName().compareTo(person2.getName()))
.map(person -> person.getAddress().getCity())
.toArray(length -> new String[length]);

This creates a stream of Person objects from a collection, then filters out all persons whose name doesn't start with "J", then sorts it by the person's name, then converts it to a String stream of city names, and finally produces an array of city-names.
Mind that this is not safe against getAddress() nulls, and there could be getCity() nulls in the resulting array!

The Stream monad has two types of operations:

  1. intermediate, produces a stream again, suitable for method-chaining
  2. terminal, closes the streaming, no method can be chained any more.

Here are some intermediates:

And here are some terminals:

Parallel Processing?

This is what it is all about. Quite easy to do now after wrapping loops:

getPersons().parallelStream()
....
// or:
personStream.parallel()
....

When you want to process multi-threaded, you need to make sure that the mapper-functions do not have side-effects.

Example Source

Click here to see an example class you can copy & paste for exploring monads.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class MonadsExample
{
public static void main(String[] args) {
Person testPerson = null;
//Person testPerson = new Person("Joe", new Address("Tokio"));

String city = Optional.ofNullable(testPerson)
.map(person -> person.getAddress())
.map(address -> address.getCity())
.orElseGet(() -> "No city!");

System.err.println("city = "+city);


Collection<Person> persons = getPersons();
String [] cities = persons.stream()
.filter(person -> person.getName().startsWith("J"))
.sorted((person1, person2) -> person1.getName().compareTo(person2.getName()))
.map(person -> person.getAddress().getCity())
.toArray(length -> new String[length]);

System.err.println("cities = "+Arrays.toString(cities));
}


private static Collection<Person> getPersons() {
final Collection<Person> names = new ArrayList<>();
names.add(new Person("Tim", new Address("Teamtown")));
names.add(new Person("Jack", new Address("Backtown")));
names.add(new Person("Joe", new Address("Downtown")));
return names;
}


private static class Person
{
private final String name;
private final Address address;

public Person(String name, Address address) {
this.name = name;
this.address = address;
}

public String getName() {
return name;
}
public Address getAddress() {
return address;
}
}

private static class Address
{
private final String city;

public Address(String city) {
this.city = city;
}

public String getCity() {
return city;
}
}

}

The output of it is


city = No city!
cities = [Backtown, Downtown]

Summary

We should just rewrite those loops in our applications to Stream that really are performance bottlenecks. We can use lambdas wherever they are supported, but keep in mind that such source code is not reusable. The Optional (null-object-pattern) surely also makes sense in many cases.

If there would be one that was programmer-friendly enough, yes. But I know none. Functional source code tends to be unreadable. Given the fact that a big part of the programming world works in languages like Python (which is a dynamically typed script language), I would say we can stay on Java another decade :-)





ɔ⃝ Fritz Ritzberger, 2016-08-21