Saturday, February 22, 2014

Functional Programming with Java 8 Lambda Expressions - Monads

What is a monad?: A monad is a design pattern concept used in mostly functional programming languages like lisp or in the modern world clojure or scala. (I would in fact copy a few things from scala.) Now why is it becoming important in java? Because java has got its new lambda feature from version 8. Lambda or closure is a functional programming feature. It allowes you to use code blocks as variables and lets you pass it around as such. I have discussed about Java's 'Project Lambda' in my previous article What's Cooking in Java 8 - Project Lambda. You can now try it out on JDK 8 preview release available in here. Now could we do monads before Java 8? Sure, after all Java's lambda is semantically just another way of implementing an interface (Its not actually that because the compiler knows where its being used), but it would be a lot messier code which would pretty much kill its utility.


Now, rather than describing an abstract and seemingly meaningless idea to you, let me set up a use-case in Java as it would be without monads.

Pesky null checks: If you have written any non-trivial (like Hello-World) java program, you have probably done some null checks. They are like the necessary evil of programming, you cannot do without them, but they make your program cluttered with noise. Lets take the following example with a set of java data objects. Notice I have not used getters or setter which are anti-patterns anyway.

public static class Userdetails{
    public Address address;
    public Name name;
    
}

public static class Name{
    public String firstName;
    public String lastName;        
}

public static class Address{
    public String houseNumber;
    public Street street;
    public City city;
    
}

public static class Street{
    public String name;        
}

public static class City{
    public String name;        
}


Now say you want to access the street name from a UserDetails user with the possibility of any property being null. Without monads, you would probably write a code like the following.

if(user == null )
    return null;
else if(user.address == null)
    return null;
else if(user.address.street == null)
    return null;
else
    return user.address.street.name;


It ideally should be a one-liner. We have so much noise around the code we really care about. So lets see how we can fix that. Let create a class Option that represents an optional value. And lets then have a map method that will run a lambda on its wrapped value and return another option. If the wrapped value is null, it will return an Option containing null without processing the lambda, thus avaoiding a null pointer exception. Note that the map method needs to actually take a lambda as a parameter, but we will need to create an interface SingleArgExpression to support that.



So now, basically the idea is to return an Option whenever a method has the chance of returning null. It will make sure that the consumer of the method understands that the value can be null and also lets the consumer move past null checks implicitly as shown. Now that we are returning Option from all our methods that might have to return null, its likely that the expressions inside the map would also have Option as return type. To avoid calling get() every time, we can have a similar method flatMap that is same as map, except it accepts an Option as a return type for the lambda that is passed to it.

    public <E> Option<E> flatMap(SingleArgExpression<T, Option<E>> mapper){
        if(value == null){
            return new Option<E>(null);
        }
        return  mapper.function(value);
        
    }


The last method I would talk about is filter. It will let us put an if condition in the map chain, so that a value is obtained only when a condition is true. Note that this is also null-safe. The use of filter is not obvious in this particular monad, but we will see its usage later. The following is a sample where all nullable fields have been upgraded to Option and hence flatMap is used instread of map.



Collections and Monads: Monads can be useful for Collection frameworks as well. Although the best way would be for every collection class to be monads themselves for best performance (Which they might become in future), currently we can wrap them up. It also creates a problem of having to break the type cheking system, because we do not know the generic return type of the builder beforehand.



At the first glance, it might appear that using flatmap is quite a bit of trouble here because we need to create a CollectionMonad from the lambda. But if you think about the equivalent code with a nested for loop, it still is pretty neat.

Streams and Monads: Well at this point you might be thinking of InputStream(s), but we would discuss something more general than this. A stream is basically a sequence which is possibly infinite. It can be created say for example using a formula, or indeed an InputStream. We will have Streams with hasNext() and next() methods just like Iterator. In fact we will use Iterator interface so we can use the enhanced for loop. But we will also make the stream monads. This case is particularly interesting because streams are possibly infinite, hence the map must return a stream that lazily processes the lambda. In our example, we would create a specialized random number generator with specific distribution. Normally all values are equally probable. But we can change that by mapping. Let see the example to better understand.

Let's create a generic Stream that can wrap an arbitrary Iterator. That way we can use it existing collection framework as well.



This example is fairly complex, so spend some time reading this. However, the Stream class needs to be created only once. Once its there, it can used to wrap any Iterator and it will give you all the monadic features for free

In my next post, I would explain some more monads.

1 comment:

  1. Also, this is a thing in Java 8 http://download.java.net/jdk8/docs/api/java/util/Optional.html

    ReplyDelete