Back to Home

The word for the letter "M", or the Monad is already here / Blog of the company CIT

java · scala · monad · monad · functional programming · functional programming · optional · stream · future

The word for the letter "M", or the Monad is already here



    There are many memes and legends about the monad. It is said that every self-respecting programmer, in the course of his functional maturity, must write at least one tutorial about the monad - it is not without reason that a special timeline is even conducted on the Haskell language website for all brave attempts to tame this mysterious beast. Experienced developers also talk about the curse of monads - they say that anyone who understands the essence of this monster completely loses the ability to explain what they saw. For this , some are armed with category theory , others put on space suits , but apparently there is no single way to get to monads, otherwise each programmer would not invent his own.

    Indeed, the concept of the monad itself is not intuitive, because it lies at such levels of abstraction that intuition simply cannot reach without proper training and theoretical preparation. But is it so important, and is there no other way? Moreover, these mysterious monads already surround many unsuspecting programmers, even those who write in languages ​​that have never been considered "functional." Indeed, if you look closely, you can find that they are already here, in the Java language, under our very nose, although we can hardly find the word "monad" in the documentation for the standard library.

    That is why it is important if you do not understand the deep essence of this pattern, then at least learn how to recognize examples of the use of the monad in the existing APIs that surround us. A concrete example always gives more than a thousand abstractions or comparisons. This article is devoted to just such an approach. There will be no category theory, and indeed no theory at all. There will be no comparisons with real world objects torn from the code. I’ll just give a few examples of how monads are already used in the familiar API, and I’ll try to give readers the opportunity to catch the main signs of this pattern. Basically, the article will talk about Java, and towards the end, in order to break out of the world of legacy restrictions, we will touch on Scala a little.

    Problem: Potential Missing Object


    Take a look at this line of Java code:

    return employee.getPerson().getAddress().getStreet();
    

    If we assume that it compiles normally in its context, the experienced eye will still notice a serious problem here - any of the returned objects in the call chain may be absent (the method will return null), and then ruthless NullPointerException will be thrown when this code is executed. Fortunately, we can always wrap this line in a bunch of checks, for example, like this:

    if (employee != null
            && employee.getPerson() != null
            && employee.getPerson().getAddress() != null) {
        return employee.getPerson().getAddress().getStreet();
    } else {
        return "<неизвестно>";
    }
    

    It doesn’t look very good in itself, but composing with other code is even worse. And most importantly, if you forget at least one check, you can get an exception at runtime. This is because the information about the potential absence of an object is not fixed in types in any way, and the compiler will not save us from an error. But after all, we just wanted to perform three simple sequential actions - take the person from the employee, the address from the person, and the street from the address. It seems to be a simple task, but the code was swollen from auxiliary checks and became unreadable.

    Fortunately, Java 8 introduced the java.util.Optional type . There are many interesting methods in it, but we'll talk about these:

    public class Optional {
        public static  Optional ofNullable(T value) { }
        public Optional map(Function mapper) { }
        public Optional flatMap(Function> mapper) { }
    }
    

    Optional can be considered as a container containing either one element or nothing. If you call the map method on this container and pass an anonymous function (lambda) or a reference to the method there, then map will apply this function to the object inside the Optional and return the result, also wrapping it in Optional. If the object does not appear inside, then map will simply return the empty Optional container again, but with a different type parameter.

    The flatMap method allows you to do the same as the map method, but it accepts functions that they themselves return Optional - then the result of using these functions will not be additionally wrapped in Optional, and we will avoid double nesting.

    Such Optional interface allows you to arrange calls in chains, for example, as follows:

    return Optional.ofNullable(employee)
            .map(Employee::getPerson)
            .map(Person::getAddress)
            .map(Address::getStreet)
            .orElse("<неизвестно>");
    

    It looks a little more compact than in the previous example. But the pros do not end there. Firstly, we removed all the husks from the code that were irrelevant - we performed a few simple actions with the employee object, describing them in the code explicitly and without unnecessary auxiliary code. Secondly, we can be sure that there is no NPE if somewhere in the path of this chain there is a null value - Optional saves us from this. Thirdly, the resulting construct is an expression (and not an assertion, like the if construct from the previous example), which means it returns a value - therefore, it is much easier to compose with other code.

    So, how did we solve the problem of the potential absence of an object using the Optional type?
    1. Explicitly indicated a problem in the object type (Optional)
    2. They hid all the auxiliary code (checking for the absence of an object) inside this type.
    3. Passed to the type a set of simple matching actions.

    What is understood by “matching actions” here? And here's what: the Person :: getAddress method accepts an object of type Person received as a result of the previous Employee :: getPerson method. Well, the Address :: getStreet method, respectively, accepts the result of the previous action - calling the Person :: getAddress method.

    And now, the main thing: Optional in Java is nothing more than an implementation of the monad pattern.

    Problem: Iteration


    With all the syntactic sugar that has appeared in Java in recent years, this would seem to be no longer a problem. However, look at this code:

    List employeeNames = new ArrayList<>();
    for (Company company : companies) {
        for (Department department : company.getDepartments()) {
            for (Employee employee : department.getEmployees()) {
                employeeNames.add(employee.getName());
            }
        }
    }
    

    Here we want to collect the names of all employees of all departments and all companies into a single list. In principle, the code does not look so bad, although the procedural style of modifying the employeeNames list will make any functional programmer grimace. In addition, the code consists of several nested iteration cycles, which are clearly redundant - with the help of them we describe the collection iteration mechanism, although by and large it is not interesting to us, we just want to collect all the people from all departments of all companies and get their names.

    Java 8 introduced a whole new API, which makes it more convenient to work with collections. The main interface of this API is the java.util.stream.Stream interface , which contains, among other things, methods that may seem familiar from the previous example:

    public interface Stream extends BaseStream> {
         Stream map(Function mapper);
         Stream flatMap(Function> mapper);
    }
    

    Indeed, the map method, as in the case of Optional, takes a function that transforms an object as an input, applies it to all elements of the collection, and returns the next Stream from the received transformed objects. The flatMap method accepts a function that itself returns a Stream, and merges all the streams received during the conversion into a single Stream.

    Using the Streams API, the iteration code can be rewritten like this:

    List streamEmployeeNames = companies.stream()
            .flatMap(Company::getDepartmentsStream)
            .flatMap(Department::getEmployeesStream)
            .map(Employee::getName)
            .collect(toList());
    

    Here we cheated a little to get around the limitations of the Streams API in Java - unfortunately, they do not replace existing collections, but are a whole parallel universe of functional collections, the portal to which is the stream () method. Therefore, we must handle each collection obtained during data processing with pens into this universe. To do this, we added getters for collections to the Company and Department classes, which immediately convert them to Stream objects:

    static class Company {
        private List departments;
        public Stream getDepartmentsStream() {
            return departments.stream();
        }
    

    The solution, if it looks more compact, is not much, but its advantages are not only this. In fact, this is an alternative mechanism for working with collections, more compact, type-safe, composable, and its advantages begin to reveal as the size and complexity of the code increase.

    So, the approach used to solve the iteration problem over the elements of collections can again be formulated in the form of several statements already familiar to us:
    1. Clearly indicated a problem in the type of object (Stream)
    2. They hid all the auxiliary code (iterating over the elements and calling the passed function above them) inside this type.
    3. We passed an object of this type a set of simple matching actions.

    To summarize: the Stream interface in Java is an implementation of the monad pattern.

    Problem: Asynchronous Computing


    From what has been said so far, it may seem that the monad pattern implies the presence of some kind of wrapper over the object (s), into which you can throw functions that convert these objects, and all the boring and unnecessary code associated with the use of these functions, handling potential errors, workarounds - describe inside the wrapper. But in fact, the applicability of the monad is even wider. Consider the problem of asynchronous computing:

    Thread thread1 = new Thread(() -> {
        String string = "Hello" + " world";
    });
    Thread thread2 = new Thread(() -> {
        int count = "Hello world".length();
    });
    

    We have two separate threads in which we want to perform calculations, and calculations in thread2 should be performed on the result of calculations in thread1. I will not even try to give here the thread synchronization code that will make this design work - there will be a lot of code, and most importantly, it will not compose well when there are many such blocks of calculations. But we just wanted to perform two simple actions one after another - but the asynchrony of their execution confuses us all the cards.

    To overcome excessive complexity, even in Java 5, Future appeared, allowing you to organize blocks of multi-threaded calculations in chains. Unfortunately, in the java.util.concurrent.Future classwe will not find the map and flatMap methods that are familiar to us - it does not implement a monadic pattern (although its implementation of CompletableFuture gets close enough to this). Therefore, here again we cheat a little and go beyond Java, and try to imagine what Future would look like if it appeared in Java 8 and leave it as a homework for readers. Consider the scala.concurrent.Future trait interface in the Scala standard library (the method signature is somewhat simplified):

    trait Future[+T] extends Awaitable[T] {
      def map[S](f: T => S): Future[S]
      def flatMap[S](f: T => Future[S]): Future[S]
    }
    

    If you look closely, the methods are very familiar. The map method applies the passed function to the result of the futur execution - when this result is available. Well, the flatMap method uses a function that returns the futur itself - so these two futures can be chained using flatMap:

    val f1 = Future {
      "Hello" + " world"
    }
    val f2 = { s: String =>
      Future {
        s.length()
      }
    }
    Await.ready(
      f1.flatMap(f2)
        .map(println),
      5.seconds
    )
    

    So, how did we solve the problem of performing asynchronous interdependent blocks of calculations?
    1. The problem was clearly indicated in the type of the object (Future [String]).
    2. They hid the entire auxiliary code (calling the next one in the chain of futures at the end of the previous one) inside this type.
    3. We passed an object of this type a set of simple matching actions (futur f2 receives an object of this type (String), which returns futur f1).

    It can be summarized that Future in Scala also implements a monad pattern.

    Summary


    A monad is a functional programming pattern that allows you to easily compose (chain) actions that could otherwise be separated by tons of unsafe auxiliary code. In addition to the above examples, in functional languages, monads are used to handle exceptional situations, work with I / O, databases, status, and much more. We implement the monad pattern in any language in which functions are objects of the first class (they can be considered as values, passed as arguments, etc.), and even in Java it comes across in some places - although in some places its implementation leaves much to be desired the best.

    For a deeper immersion in the topic, I recommend the following resources:

    Read Next