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.
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.
Monads can
null
, andMoreover they enable us to program in a Fluent Interface style. Other authors call this "pipeline", or "programmed semicolons".
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.
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.
A monad is a wrapper over a given object. That given object also could be a collection of objects.
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:
map(function)
flatMap(function)
filter(predicate)
get()
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:-)
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:
Here are some intermediates:
map(function)
flatMap(function)
filter(predicate)
sorted(comparator)
distinct()
And here are some terminals:
reduce(initial, accumulator)
forEach(consumer)
count()
toArray()
IntFunction
lambda 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.
Click here to see an example class you can copy & paste for exploring monads.
1 | public class MonadsExample |
The output of it is
city = No city!
cities = [Backtown, Downtown]
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