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
- help to avoid method-calls on objects that are
null
, and - 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:
map(function)
flatMap(function)
calls a function on the wrapped object and returns a new monad built from its return,filter(predicate)
returns an empty monad when the wrapped object does not conform to the condition given by predicate,get()
returns the wrapped value, or throws an exception when null,
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:
- intermediate, produces a stream again, suitable for method-chaining
- terminal, closes the streaming, no method can be chained any more.
Here are some intermediates:
map(function)
flatMap(function)
like described abovefilter(predicate)
returns a monad-stream containing only those elements that conform to given predicate (condition)sorted(comparator)
returns a monad-stream containing the elements in a sort order produced by given comparatordistinct()
returns a stream that does not contain duplicates any more
And here are some terminals:
reduce(initial, accumulator)
computes a result object from given initial value and what the accumulator returns from being called on each elementforEach(consumer)
loops the stream with given element-consumercount()
returns the number of elements in the streamtoArray()
returns an array of the objects in the stream, optionally typed when you give anIntFunction
lambda
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
- So do we have to implement Java applications now in monadic style?
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.
- Wouldn't it be better to change to some real functional programming language?
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 :-)
Keine Kommentare:
Kommentar veröffentlichen