This is the 7th day of my participation in the November Gwen Challenge. Check out the details: The last Gwen Challenge 2021

[Note]Beautiful World of Monads – DEV Community

Let me start with the disclaimer. From a functional programming perspective, the following explanations are by no means exact or absolute. Instead, I’ll focus on clarity and simplicity in order to get as many Java developers as possible into this beautiful world.

When I started delving into functional programming a few years ago, I quickly discovered that there was a wealth of information that was almost incomprehensible to the average Java developer with an almost entirely imperative background. That is slowly changing. For example, there are many articles explaining such basic FP concepts (see introduction to Practical Functional Java (PFJ)) and how they apply to Java. Or articles that explain how to use Java flows properly. But Monads is still outside the focus of these articles. I don’t know why this is happening, but I will try to fill the gap.

So, what is Monad?

Monad is… A design pattern. It’s that simple. This design pattern consists of two parts:

  • Monad is a container of values. For each Monad, there are ways to wrap values into Monad.
  • Monad implements “inversion of control” for internally contained values. To achieve this, Monad provides methods to accept functions. These functions accept values of the same type as those stored in Monad and return converted values. The transformed value is wrapped in the same Monad as the source value.

To understand the second part of the pattern, we can look at Monad’s interface:

interface Monad<T> {
    <R> Monad<R> map(Function<T, R> mapper);

    <R> Monad<R> flatMap(Function<T, Monad<R>> mapper);
}
Copy the code

Of course, specific Monads often have richer interfaces, but these two methods should definitely exist.

At first glance, accepting functions instead of accessing values doesn’t make much difference. In fact, this gives Monad complete control over how and when the transformation functionality is applied. When you call the getter, you want to get the value immediately. In the case of Monad transformations it can be applied immediately or not at all, or its application can be delayed. The lack of direct access to internal values enables Monad to represent even values that are not yet available!

I’ll show you some examples of Monads and what problems they can solve.

Monad missing values or Optional/Maybe scenarios

This Monad has many names — Maybe, Option, Optional. That last one sounds familiar, doesn’t it? Well, because Java 8 Optional is part of the Java platform.

Unfortunately, the Java Optional implementation reveres traditional imperative methods too much, making it less useful. In particular, Optional allows applications to get values using the.get() method. If a value is missing, an NPE is even thrown. Thus, the use of Optional is generally limited to indicating the return of potential missing values, although this is only a small subset of potential uses.

Perhaps the purpose of Monad is to represent values that can be lost. Traditionally, this role has been reserved for NULL in Java. Unfortunately, this leads to a number of different problems, including the famous NullPointerException.

For example, if you expect some arguments or some return values to be null, you should check it before using it:

public UserProfileResponse getUserProfileHandler(final User.Id userId) {
    final User user = userService.findById(userId);
    if (user == null) {
    return UserProfileResponse.error(USER_NOT_FOUND);
    }
   
    final UserProfileDetails details = userProfileService.findById(userId);
   
    if (details == null) {
    return UserProfileResponse.of(user, UserProfileDetails.defaultDetails());
    }
   
    return UserProfileResponse.of(user, details);
}
Copy the code

Look familiar? Of course.

Let’s see how Option Monad changes this (using a static import for brevity) :

    public UserProfileResponse getUserProfileHandler(final User.Id userId) {
        return ofNullable(userService.findById(userId))
                .map(user -> UserProfileResponse.of(user,
                        ofNullable(userProfileService.findById(userId)).orElseGet(UserProfileDetails::defaultDetails)))
                .orElseGet(() -> UserProfileResponse.error(USER_NOT_FOUND));
    }
Copy the code

Note that the code is cleaner and less “intrusive” to the business logic.

This example shows how convenient Monadic “inversion of control” can be: transformations don’t check for NULL, and values are called only when they are actually available.

“Do something if/when available” is the key mindset to start using Monads easily.

Note that the above example preserves the full content of the original API. But it makes sense to use the method more widely and change the API, so they return Optional instead of NULL:

    public Optional<UserProfileResponse> getUserProfileHandler4(final User.Id userId) {
        return optionalUserService.findById(userId).flatMap(
                user -> userProfileService.findById(userId).map(profile -> UserProfileResponse.of(user, profile)));
    }
Copy the code

Some observations:

  • The code is cleaner and contains almost zero boilerplate.
  • All types are automatically derived. This is not always the case, but in the vast majority of cases, types are derived from the compiler — although type inference is weaker in Java than in Scala.
  • There is no clear error handling, but instead we can focus on the happy days scenario.
  • All transformations are easily composed and linked without interrupting or interfering with the main business logic.

In fact, the above properties are common to all Monads.

To throw or not to throw is a question

Things don’t always go our way, and our apps live in the real world, full of pain, mistakes and missteps. Sometimes we can do something with them, sometimes we can’t. If we can’t do anything, we at least want to notify the caller that things didn’t go as we expected.

In Java, we have traditionally had two mechanisms for notifying callers of problems:

  • Returns a special value (usually null)
  • An exception is thrown

In addition to returning NULL, we can return Option Monad (see above), but this is usually not sufficient because more details about the error are required. Normally we would throw an exception in this case.

But there’s a problem with this approach. In fact, there are very few problems.

  • Abnormal interruption of the execution process
  • The abnormality adds a lot of psychological overhead

The psychological cost of an abnormality depends on the type of abnormality:

  • Checking for exceptions forces you to either handle them here, or declare them in the signature and shift the trouble to the caller
  • Unchecked exceptions cause the same level of problems, but are not supported by the compiler

I don’t know which is worse.

Either Monad

Let’s analyze the problem first. What we want to return is some special value, which can be one of two possible things: a result value (on success) or an error (on failure). Note that these things are mutually exclusive — if we return a value, we don’t need to carry an error, and vice versa.

That’s a nearly accurate description of Either Monad: any given instance contains only one value, and that value has one of two possible types.

Any Monad interface can be described as follows:

interface Either<L.R> {
    <T> Either<T, R> mapLeft(Function<L, T> mapper);

    <T> Either<T, R> flatMapLeft(Function<L, Either<T, R>> mapper);

    <T> Either<L, T> mapLeft(Function<T, R> mapper);

    <T> Either<L, T> flatMapLeft(Function<R, Either<L, T>> mapper);
}
Copy the code

This interface is rather verbose because it is symmetric in terms of left and right values. For narrower use cases, when we need to pass success or error, this means we need to agree on some convention — which type (first or second) will hold errors and which will hold values.

In this case, the symmetrical nature of Either makes it more error-prone, since it’s easy to swap error and success values in code unintentionally.

Although this problem is likely to be caught by the compiler, it is best tailored to this particular use case. If we fix one of these types, we can do that. Obviously, fixing error types is more convenient because Java programmers are used to deriving all errors and exceptions from a single Throwable type.

Result Monad – Either Monad dedicated to error handling and propagation

So, let’s assume that all errors implement the same interface, and we call that a failure. Now we can simplify and reduce interfaces:

interface Result<T> {
    <R> Result<R> map(Function<T, R> mapper);

    <R> Result<R> flatMap(Function<T, Result<R>> mapper);
}
Copy the code

The Result Monad API looks very similar to the Maybe Monad API.

Using Monad, we can rewrite the previous example:

    public Result<UserProfileResponse> getUserProfileHandler(final User.Id userId) {
        return resultUserService.findById(userId).flatMap(user -> resultUserProfileService.findById(userId)
                .map(profile -> UserProfileResponse.of(user, profile)));
    }
Copy the code

Well, it’s basically the same as the example above, with the only change being Monad — Result instead of Optional. Unlike the previous examples, we have complete information about the error, so we can do something at the top. However, while the full error handling code is still simple and focused on business logic.

“Commitment is an important word. It either makes or breaks something.”

The next Monad I want to show you is going to be Promise Monad.

I have to admit, I haven’t found a definitive answer as to whether Promise is monad. Different authors have different views on this. I look at it purely from a practical point of view: it looks and behaves very similar to other Monads, so I consider them a Monad.

The Promise Monad represents a value that may not yet be available. In a sense, it is very similar to Maybe Monad.

Promise Monad can be used to represent, for example, the result of a request to an external service or database, a file read or write. Basically it can represent anything that requires I/O and time to execute it. Promise supports the same way of thinking we observed in other Monads — “do something if/when value is available”.

Note that since there is no way to predict the success of an operation, it is convenient to have a Promise represent not the value itself, but a Result with a value inside it.

To see how it works, let’s look at the following example:

.public interface ArticleService {
    // Returns list of articles for specified topics posted by specified users
    Promise<Collection<Article>> userFeed(final Collection<Topic.Id> topics, finalCollection<User.Id> users); }...public interface TopicService {
    // Returns list of topics created by user
    Promise<Collection<Topic>> topicsByUser(final User.Id userId, finalOrder order); }...public class UserTopicHandler {
    private final ArticleService articleService;
    private final TopicService topicService;

    public UserTopicHandler(final ArticleService articleService, final TopicService topicService) {
        this.articleService = articleService;
        this.topicService = topicService;
    }

    public Promise<Collection<Article>> userTopicHandler(final User.Id userId) {
        returntopicService.topicsByUser(userId, Order.ANY) .flatMap(topicsList -> articleService.articlesByUserTopics(userId, topicsList.map(Topic::id))); }}Copy the code

I’ve included two necessary interfaces to provide the entire context, but the interesting part is actually the userTopicHandler() method. Although the simplicity of this approach is questionable:

  • Call TopicService and retrieve the list of topics created by the supplied user
  • After successfully retrieving the list of topics, the method extracts the topic ID and then calls ArticleService to get the list of articles created by the user for the specified topic
  • Perform end-to-end error handling

Afterword.

Monads is a very powerful and convenient tool. Writing code with the “do it when the value is available” mindset takes some getting used to, but once you start using it, it will make your life a lot easier. It allows a lot of psychological overhead to be offloaded to the compiler and makes many errors impossible or detectable at compile time rather than at run time.