📜 ⬆️ ⬇️

The word “M”, or Monads, is already here.



About the monad goes a lot of memes and legends. They say that every self-respecting programmer during his functional maturation must write at least one tutorial about the monad - it’s not for nothing that the Haskell language even has a special timeline for all brave attempts to tame this mysterious beast. Experienced developers also talk about the curse of monads - they say, everyone who comprehends the essence of this monster, completely loses the ability to explain what they have seen. Some are armed with the theory of categories for this, others put on space suits , but apparently there is no single way to get to monads, otherwise every programmer would not invent his own.

Indeed, the concept of the monad itself is non-intuitive, because it lies at such levels of abstraction that intuition simply does not reach without proper training and theoretical preparation. But is it 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 may find that they are already here, in the Java language, right under our very nose, although in the documentation on the standard library we can hardly find the word “monad”.
')
That is why it is important if you do not comprehend the deep essence of this pattern, then at least learn to recognize examples of using 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 this approach. There will be no category theory, and indeed no theory. There will be no comparisons with real-world objects divorced from code. I'll just give a few examples of how monads are already used in the familiar API, and I will try to give readers the opportunity to catch the main features of this pattern. Basically, the article will deal with Java, and towards the end, in order to break out of the world of legacy restrictions, we'll touch Scala a bit.

Problem: potential object absence


Let's look at this line of Java code:

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

If we assume that it compiles normally in its context, an experienced eye will notice a serious problem anyway - any of the returned objects in the call chain may be missing (the method returns null), and then when executing this code, a ruthless NullPointerException will be thrown. 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 looks not very good by itself, but it is composited with another code and 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 the object is not fixed in any way in the types, 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 has become bloated from auxiliary checks and has become unreadable.

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

 public class Optional<T> { public static <T> Optional<T> ofNullable(T value) { } public<U> Optional<U> map(Function<? super T, ? extends U> mapper) { } public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) { } } 

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

The flatMap method allows you to do the same thing as the map method, but it accepts functions that return Optional — then the result of applying these functions will not be wrapped further in Optional, and we will avoid double nesting.

This interface Optional allows you to build calls in the chain, for example, as follows:

 return Optional.ofNullable(employee) .map(Employee::getPerson) .map(Person::getAddress) .map(Address::getStreet) .orElse("<>"); 

It looks a bit more compact than in the previous example. But the pros do not end there. First, we removed all the unrelated husks from the code - we performed several simple actions with the employee object, describing them explicitly in the code and without unnecessary auxiliary code. Secondly, we can be sure that there is no NPE, if a null-value occurs somewhere along the path of this chain — Optional saves us from this. Third, the resulting construction is an expression (and not a statement, as if construction from the previous example), which means it returns a value — hence, it is much easier to compose with another code.

So, how did we solve the problem of the potential absence of an object using the Optional type?
  1. Marked explicitly the problem in the type of the object (Optional <Employee>).
  2. Hid all the auxiliary code (checking for the absence of an object) inside this type.
  3. A type was passed to a set of simple matching actions.

What is meant by “joining actions”? Here's what: the Person :: getAddress method takes as input the object of type Person, obtained as the result of the previous method Employee :: getPerson. Well, the Address :: getStreet method, respectively, accepts the result of the previous action - the call to the Person :: getAddress method.

And now the main thing: Optional in Java is nothing more than the 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<String> 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 in 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 grim. In addition, the code consists of several nested search cycles, which are obviously redundant - with the help of them we describe the collection iteration mechanism, although we are not interested in it, we just want to collect all people from all departments of all companies and get their names.

Java 8 has a whole new API that allows you to more conveniently 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<T> extends BaseStream<T, Stream<T>> { <R> Stream<R> map(Function<? super T, ? extends R> mapper); <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper); } 

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

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

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

Here we are a little clever to get around the limitations of 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, each collection obtained in the course of data processing must be carried by handles 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<Department> departments; public Stream<Department> getDepartmentsStream() { return departments.stream(); } 

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

So, the approach used to solve the problem of iteration over the elements of collections can again be formulated as several statements already familiar to us:
  1. Marked clearly the problem in the type of object (Stream <Company>).
  2. Hid all the auxiliary code (iteration over elements and a call of the transferred function over them) inside this type.
  3. A set of simple matching actions was passed to an object of this type.

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

Problem: asynchronous computing


From what has been said so far, one might get the impression that the monad pattern implies the presence of some kind of wrapper over an object (objects) into which you can throw functions that convert these objects, and all the boring and unnecessary code associated with using these functions, processing potential errors, bypass mechanisms - describe inside the wrapper. But in fact, the monad's applicability 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 thread 2 must be made on the result of calculations in thread 1. I will not even try to give here a thread synchronization code that will make this design work - there will be a lot of code, and most importantly, it will be poorly composited when there are many such computing units. But we just wanted to perform two simple actions one after the other - but the asynchronous execution confuses all the cards.

To overcome the unnecessary complexity, futures (Future) appeared in Java 5, allowing to organize blocks of multi-threaded calculations in chains. Unfortunately, in the java.util.concurrent.Future class, we will not find the map and flatMap methods we are familiar with — it does not implement the monadic pattern (although its implementation of the CompletableFuture comes close enough to this). Therefore, here again we will cheat a little and go beyond Java, and trying to present what the Future interface would look like, if it appeared in Java 8, leave it as a home task to readers. Consider the treit interface scala.concurrent.Future in the standard library of the Scala language (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 future 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 running asynchronous interdependent computing units?
  1. Marked clearly the problem in the type of object (Future [String]).
  2. They hid all the auxiliary code (call the next one in the chain of futures at the end of the previous one) inside this type.
  3. A set of simple matching actions was passed to an object of this type (future f2 takes an object of this type (String), which returns future f1).

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

Results


A monad is a functional programming pattern that makes it easy and without side effects to compose (build in chains) actions that otherwise could be separated by tons of unsafe auxiliary code. In addition to the examples given, in functional languages, monads are used to handle exceptional situations, work with input-output, databases, states, and many other places. The monad pattern can be implemented in any language in which functions are first-class objects (they can be treated as values, passed as arguments, etc.), and even in Java it already comes across somewhere - although its implementation leaves some to wish for the best.

For a deeper dive into the topic I recommend the following resources:

Source: https://habr.com/ru/post/262055/


All Articles