Functional programming with Java Monads.

What are Monads?
Monads are simple yet powerful types that could give better structure to the code and in some cases can also help dealing with unwanted side effects. In this series of posts I will try to explain what monads are by showing some examples using Java, including a simplified version of the Result monad as an alternative to use Java exceptions for control flow.

What is a monad?

Technically, a monad is a parameterised type such as Optional and Stream in Java which:
  • Implements flatMap (a.k.a. bind) and unit (a.k.a. identity, return, Optional.of(), etc…).
  • Follows three laws: Left identity, Right identity and associativity, which are out of the scope of this post[1].
For the sake of aligning the terminology, let’s say that a parameterised type such as Optional<String> has a type parameter: String and a wrapper type: Optional. In this case we say that “String type is wrapped by an Optionalcontext”.

How are monads useful?

Monads are all about composition of parametrised types. Let’s take the Optional monad[2] as an example. We know that to be able to manipulate the parameter properly, we should at least check whether the optional is not empty and then get the value out of it. Also, it’s a good practice to do something in case the Optional is empty. We’ll call this entire process unwrapping the parameter from the Optional context.
Let’s write some code. First, let’s try adding two Optional numbers. If one of them is empty, then return empty, otherwise add them and return an Optional. Classic.
Notice how we had to manually “unwrap the Integer from the Optional context”by checking whether the numbers are present (line 2), and doing something with the empty case (line6).
This is precisely what monads helps us to do: To avoid dealing with the context when composing parametrised types. In the next example, the context refers to the Optional context, and composition refers to addition, so we can say: The Optional monad help us to avoid dealing with the Optional context when adding Optional types.
They do so thanks to the methods flatMap (a.k.a bind) and Optional.of (a.k.a unit). The flatMap method let us take the parameter from the monad and operate with it to produce another Monad of the same type. Because of this signature, we can nest flatMap calls on different monads to compose their parameters. Also, Optional.of (a.k.a unit) is a very handy method that will let us take any value and produce another Optional. Look at this:



1 — Bear with me with the weird indentation, it’s for didactical purposes. I will present an alternative later in this post.
2 — The flatMap method receives a Function. We’ll call this function the “mapper” function.
Notice how we didn’t need to check whether the values are present and deal with the empty case. The Optional monad via its flatMap method properly unwraps the parameter for us. It won’t invoke the second flatMap mapper function if the first Optional is empty. In fact, if any of the Optionals involved in the composition is empty, the result will be an empty Optional.
Also look how “val2.flatMap” (line 4) is nested inside the mapper function of “val1.flatMap” (line 3). This is because the main operation “first + second” (line 5) needed two parameters from two different Optionals. The mapper function of the nested Optional (val2) will have both parameters in scope[3].
Now, this is more than just syntax sugar (although it looks like it is because the Optional monad is a very simple one), this approach actually allow us to deal with the type parameter of monads without even needing to know how to unwrap it. We didn’t have to think what to do with the empty case, or how do we get the value out of the monad, or what would happen if we ever dare to get the value of an empty Optional. This is knowledge that optionalAddmethod doesn’t need to have to be able to compose the monads. Such property becomes more important when unwrapping a monad parameter is not so simple. I will emphasise on this when we discuss the Result monad in the next post.
Meanwhile, let’s discuss another Optional example. We all know that nested Optionals can get a bit cumbersome to handle. Look at this:


Here we have in lines 4 and 5, two methods that return an Optional<Counter> monad and we want to calculate the total colourCountvalue of the two counters.
Look at the totalColourCount method (starting on line 8). To access the colourCount property of an Optional<Counter>, I need to unwrap Counterand then unwrap colourCount (line 13–19).
With flatMap and a bit of Java 8 syntax sugar, we can avoid handling the Optional context.


Here again the Optional monad helped us dealing with the context. This time, notice how we chained flatMap calls to unwrap the nested colour Optional property, and then we nested the other Optional colour (line 7) to be able to compose the monad (execute the operation on line 8).

Furthermore, fetchPreviousMonthCounts() (line 7) is only executed iffetchThisMonthCounts() (line 6) returns a non empty Optional. We have this nice feature for free thanks to the way the Optional monad implements composition via flatMap.
You can find the complete implementation of the Optional examples here and some tests showing the different scenarios here.

Wrapping it up

Monads are types that have a standard way of composition in which the context manipulation is handled by the Monad itself. It is standard because any parametrised type that satisfies the monad laws[1] can be composed in the same way.
In Java, the Optional type follows all the monad laws[1] and by nesting and chaining its flatMap method we can compose it nicely. Like Optional, Java provides other monadic types such as Stream and CompletableFuture. They behave in similar way to Optional in that they follow the monad laws and so, they are equally composable.
In the next post I will try to show more explicitly the impact of not having to deal with the context by discussing the Result monad. Also we will analyse how it can be used as an alternative to Java checked exceptions. We will then add side-effects-free logging capabilities to it and modify the flatMap method to be able to support composition of the log and comply with the monads laws.

Stream and Optional classes, added to Java 8, allow you to have some fun with functional programming. The problem is that Java is still missing quite a lot to be taken seriously as a functional programming language. Lambda notation and two monads (Optional and Stream) are just the tip of the iceberg. This leads to libraries like vavr or functionaljava — both deriving from the purely functional language Haskell.
One of the first things you need to get rid of when trying to be more functional is the attempt to unwrap monads too early. It usually involves using methods like Optional.get() or Stream.collect() where there is no need yet. Sometimes, though, Java doesn't help with that, so let me give you some custom code. This article will bring you closer to the concept of method lifting.

Calculations on Optionals

Suppose we have a nice API we would like to use to calculate numbers:
public interface Math {
    int multiply(int a, int b);
    double divide(int a, int b);
    ..
}


We would like to use it to do some calculations on numbers wrapped with Optional:
public interface NumberProvider {
    Optional<Integer> getNumber();
}


Let's say we want to write a method, which returns the result of the division of two numbers wrapped with Optional, or empty Optional if any one of them is empty (we skip the zero divisor case here). It may look something like this:
public Optional<Double> divideFirstTwo(NumberProvider numberProvider, Math math) {
    Optional<Integer> first = numberProvider.getNumber();
    Optional<Integer> second = numberProvider.getNumber();
    if(first.isPresent() && second.isPresent()) {
        double result = math.divide(first.get(), second.get());
        return Optional.of(result);
    } else {
        return Optional.empty();
    }
}


That's rather nasty. It involves a lot of code, the only purpose of which is to wrap/unwrap the Optional. Let's try to make it more functional:
public Optional<Double> divideFirstTwo(NumberProvider numberProvider, Math math) {
    return numberProvider.getNumber()
           .flatMap(first -> numberProvider.getNumber()
                                     .map(second -> math.divide(first, second)));
}


That's much better. It turns out that invoking flatMap on the first monad and map on the second one inside the lambda can be extracted even further to the method-called lift:
public interface Optionals {
    static <R, T, Z> BiFunction<Optional<T>, Optional<R>, Optional<Z>> lift(BiFunction<? super T, ? super R, ? extends Z> function) {
        return (left, right) -> left.flatMap(leftVal -> right.map(rightVal -> function.apply(leftVal, rightVal)));
    }
}


The lift is able to promote any function, which takes two arguments, to the function with the arguments and the result type wrapped with Optional. It actually adds Optional behavior to the function in such a way that if one of the arguments is empty, then the result will also be empty. If the JDK extracted flatMap and map methods to some common interface, for example, Monad, then we could write one lift function for every instance of a Java monad (Stream, Optional, custom classes). Unfortunately, we need to do this copy-pasting for every instance. 

Source:

Kommentare

Beliebte Posts