The beautiful world of Java design pattern Monads

Keywords: Java Design Pattern

[note] this article is translated from: [Beautiful World of Monads - DEV Community](
https://dev.to/siy/beautiful-...)

Let me start with the disclaimer. From the perspective of functional programming, the following explanation is by no means accurate or absolutely accurate. On the contrary, I will focus on the clarity and simplicity of the explanation, so as to allow as many Java developers as possible to enter this beautiful world.

A few years ago, when I began to delve into functional programming, I soon found that there was a lot of information, but it was almost incomprehensible to ordinary Java developers with almost complete imperative background. Now, the situation is slowly changing. For example, there are many articles explaining, for example, basic FP concepts (reference: Introduction to practical functional Java (PFJ) )And how they apply to Java. Or articles explaining how to use Java streams correctly. But Monads is still not the focus of these articles. I don't know why this happens, but I will try to fill this 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 in 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 the converted value. The converted 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);
}

Of course, specific monads usually have richer interfaces, but these two methods should definitely exist.

At first glance, accepting a function rather than accessing a value doesn't make much difference. In fact, this gives monad complete control over how and when to apply the conversion function. When you call getter, you want to get the value immediately. In the case of monad conversion, 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 Enough to represent values that are not even available!

Below I will show some examples of Monad and what problems they can solve.

Monad missing value or Optional/Maybe scene

This Monad has many names - Maybe, Option, Optional. The last one sounds familiar, doesn't it? Well, because Java 8 Option is a part of the Java platform.

Unfortunately, the Java Optional implementation has too much respect for traditional imperative methods, which makes it less useful. In particular, Optional allows applications to use the. get() method to obtain values. If values are missing, NPE will even be thrown. Therefore, the use of Optional is usually limited to indicating the return of potentially missing values, although this is only a small part of the potential use.

Perhaps Monad's purpose is to represent values that may be lost. Traditionally, this role in Java is reserved for null. Unfortunately, this leads to many different problems, including the famous NullPointerException.

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

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);
}

Does it look familiar? Of course.

Let's see how Option Monad changes this (for brevity, use a static import):

    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));
    }

Note that the code is more concise and less "interference" with business logic.

This example shows how convenient monadic's "inversion of control" is: transformations do not need to check for null s, and they are called only when values are actually available.

"Do something if / when the value is available" is the key mentality to start using Monads easily.

Note that the above examples retain the full content of the original API. However, it makes sense to use this method more widely and change the API, so they will 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)));
    }

Some observations:

  • The code is more concise and contains almost zero templates.
  • All types are derived automatically. Although this is not always the case, in most cases, types are derived by the compiler - although type inference in Java is weaker than in Scala.
  • There is no clear error handling, but we can focus on the "happy day scene".
  • All transformations are easily combined and linked without interrupting or interfering with the main business logic.
    In fact, the above attributes are common to all monads.

Whether to throw or not is a question

Things are not always what we want. Our applications live in the real world, full of pain, errors and mistakes. Sometimes we can do something with them, sometimes we can't. If we can't do anything, we at least want to inform the caller that things are not going as we expected.

In Java, we traditionally have two mechanisms to notify callers of problems:

  • Returns a special value (usually empty)
  • Throw exception
    In addition to returning null, we can also return Option Monad (see above), but this is usually not enough because we need more details about the error. Usually in this case, we throw an exception.

But there is a problem with this method. In fact, there are even few problems.

  • Abnormal interrupt execution process
  • Abnormal increases a lot of psychological expenses
    The psychological cost caused by the exception depends on the type of exception:
  • Checking for exceptions forces you to either handle them here or declare them in the signature and transfer the trouble to the caller
  • Unchecked exceptions cause the same level of problems, but the compiler does not support them
    I don't know which is worse.

Either Monad is here

Let's analyze this problem first. We want to return some special values, which can be one of two possible things: the result value (when successful) or the error (when failed). Note that these things are mutually exclusive - if we return a value, we don't need to carry an error, and vice versa.

The above is an almost accurate description of Either Monad: any given instance contains only one value, and the 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);
}

The interface is rather verbose because it is symmetrical in terms of left and right values. For narrower use cases, when we need to pass success or error, this means that we need to agree on a convention - which type (first or second) will save errors and which will save values.

In this case, Either's symmetry makes it more error prone because it is easy to inadvertently exchange error and success values in the code.

Although this problem is likely to be captured by the compiler, it is best tailored for this particular use case. If we fix one of these types, we can do this. Obviously, it's easier to fix error types 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, which we call 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);
}

The Result Monad API looks very similar to the API of May monad.

Using this 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)));
    }

Well, it's basically the same as the above example. The only change is Monad - Result instead of Optional. Unlike the previous example, we have complete information about the error, so we can do something at the top. However, although the complete error handling code is still simple and focused on business logic.

"Commitment is a very important word. It either achieves something or destroys something."

The next Monad I want to show will be Promise Monad.

It must be admitted that I have not found an authoritative answer to whether Promise is monad. Different authors have different views on this. I look at it purely from a practical point of view: its appearance and behavior are very similar to other monads, so I think they are a monad.

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

Promise Monad can be used to represent, for example, request results for external services or databases, file reading or writing, etc. 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".

Please note that since it is impossible to predict the success of the operation, it is convenient for Promise to represent not the value itself, but the value inside the Result.

To understand 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, final Collection<User.Id> users);
}
...
public interface TopicService {
    // Returns list of topics created by user
    Promise<Collection<Topic>> topicsByUser(final User.Id userId, final Order 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) {
        return topicService.topicsByUser(userId, Order.ANY)
                .flatMap(topicsList -> articleService.articlesByUserTopics(userId, topicsList.map(Topic::id)));
    }
}

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

  • Call TopicService and retrieve the list of topics created by the provided user
  • After successfully getting the theme list, the method extracts the theme 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

    Postscript

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

Posted by CrowderSoup on Wed, 10 Nov 2021 13:37:45 -0800