头图

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

When I started to delve into functional programming a few years ago, I quickly discovered that there was a lot of information, but it was almost incomprehensible to ordinary Java developers with almost entirely imperative backgrounds. Today, the situation is slowly changing. For example, there are many articles explaining, for example, basic FP concepts (reference: Practical Functional Java (PFJ) ) and how they apply to Java. Or an article explaining how to use Java streams correctly. But Monads is still not outside 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 some ways to wrap the value into the monad.
  • Monad implements "inversion of control" for the values contained inside. In order to achieve this, Monad provides a method to accept functions. These functions accept values of the same type as stored in Monad and return the converted value. The converted value is packed into the same monad as the source value.
    In order to understand the second part of the pattern, we can look at the Monad interface:
interface Monad<T> {
    <R> Monad<R> map(Function<T, R> mapper);

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

Of course, a particular Monad usually has a richer interface, but these two methods should definitely exist.

At first glance, it doesn't make much difference to accept a function rather than access a value. In fact, this allows Monad to fully control how and when the conversion function is applied. When you call the 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 allows monads to represent values that are not even available yet!

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

Monad missing value or Optional/Maybe scenario

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

Unfortunately, the Java Optional implementation too respects the traditional imperative approach, which makes it less useful. In particular, Optional allows applications to use the .get() method to get the value. If the value is missing, even NPE will be thrown. Therefore, the use of Optional is usually limited to the return of potentially missing values, although this is only a small part of the potential usage.

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

For example, if you expect certain parameters or certain 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, a static import is used):

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

Please note that the code is more concise and has less "interference" to the business logic.

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

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

Please note that the above example retains the complete content of the original API. But it makes sense to use the 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 boilerplate.
  • All types are derived automatically. Although this is not always the case, in the vast majority of cases, types are derived from the compiler---although type inference in Java is weaker compared to Scala.
  • There is no clear error handling, but we can focus on the "happy day scene".
  • All conversions are easily combined and linked without interrupting or interfering with the main business logic.
    In fact, the above attributes are common to all monads.

To throw or not to throw is a question

Things are not always what we want, our apps live in the real world, full of pain, mistakes and mistakes. 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 are not going as we expected.

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

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

But this method has a problem. In fact, there are even very few problems.

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

Either Monad is here

Let us analyze this problem first. What we want to return is some special value, which can be one of two possible things: the result value (in case of success) or an error (in case of failure). Please 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 the Either Monad: any given instance contains only one value, and that value has one of two possible types.

Any Monad interface can be described like this:

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 quite verbose because it is symmetrical in terms of left and right values. For narrower use cases, when we need to pass success or error, it means that we need to agree on a certain convention-which type (the first or second) will save the error and which will save the value.

In this case, the symmetrical nature of Either 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 caught by the compiler, it is best to tailor it for this specific use case. If we fix one of these types, we can do this. Obviously, it is more convenient 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 us assume that all errors implement the same interface, which we call failure. Now we can simplify and reduce the interface:

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 Maybe Monad's API.

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 is basically the same as the example above, 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 on the upper level. However, although the complete error handling code is still simple and focused on business logic.

"Promise is a very important word. It either accomplishes something or destroys something."

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

It must be admitted that I have not found an authoritative answer to whether Promise is a monad. Different authors have different opinions 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 (maybe unavailable) value. In a sense, it is very similar to Maybe Monad.

Promise Monad can be used to indicate, for example, the result of a request to an external service or database, file reading or writing, etc. Basically it can mean anything that requires I/O and time to execute it. Promise supports the same way of thinking that we have observed in other monads-"do something if/when value is available".

Please note that since it is impossible to predict whether the operation will succeed or not, it is convenient for the Promise to express 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)));
    }
}

To provide the entire context, I included two necessary interfaces, but the interesting part is actually the userTopicHandler() method. Although the simplicity of this method is doubtful:

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

    postscript

    Monads are very powerful and convenient tools. Writing code using the "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 offloaded to the compiler, and makes many errors impossible or detectable at compile time rather than at runtime.


This article is translated from: [Beautiful World of Monads-DEV Community](
https://dev.to/siy/beautiful-world-of-mondas-2cd6)


信码由缰
65 声望8 粉丝

“码”界老兵,分享程序人生。