Let me start with a disclaimer. The explanation below in no way pretends to be precise or absolutely accurate from Functional Programming's perspective. Instead, I’m focusing on clarity and simplicity of the explanation to let as many Java developers get into this beautiful world.
When I started digging into Functional Programming a few years ago, I’ve quickly discovered that there is an overwhelming amount of information, but very little of it is understandable to the average Java developer with an almost exclusively imperative background. These days situation is slowly changing. There are countless articles which explain, for example, basic FP concepts and how they are applicable to Java. Or articles explaining how to use Java streams properly. But Monads still remain out of the focus of these articles. I don’t know why this happens, but I’ll try to fill this gap.
What Is Monad, Anyway?
The Monad is … a design pattern. As simple as that. This design pattern consists of two parts:
- Monad is a container for some value. For every Monad, there are methods which allow wrap value into Monad.
- Monad implements “Inversion of Control” for the value contained inside. To achieve this, Monad provides methods which accept functions. These functions take a value of the same type as stored in Monad and return a transformed value. The transformed value is wrapped into the same kind of Monad as the source one.
To understand the second part of the pattern, it will be convenient to look at the imaginable 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 far more rich interface, but these two methods definitely should be present.
At first look, accepting functions instead of providing access to value is not a big difference. In fact, this enables Monad to retain full control on how and when to apply the transformation function. When you call a getter, you expect to get value immediately. In the case of Monad, transformation can be applied immediately or not applied at all, or its application can be delayed. Lack of direct access to value inside enables the monad to represent value which is even not yet available!
Below I’ll show some examples of Monads and which problems they can address.
The Story of The Missing Value or Option/Maybe Monad
This Monad has many names — Maybe, Option, Optional. The last one sounds familiar, isn’t it? Well, since Java 8 Optional is a part of the Java Platform.
Unfortunately Java Optional implementation makes too much reverences to traditional imperative approaches and this makes it less useful than it can be. In particular Optional allows application to get value using .get() method. And even throw NPE if value is missing. As a consequence Optional usage is often limited to represent return potentially missing value, although this is only small part of the potential usages.
The purpose of the Maybe Monad is to represent value which potentially can be missing. Traditionally, this role in Java is reserved for null. Unfortunately, this causes numerous various issues, including the famous NullPointerException.
If you expect that, for example, some parameter or some return value can be null, you ought to check it before use:
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);
}
Looks familiar? Sure it does.
Let's take a look how Option Monad changes this (with one 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));
}
Note that code is much more concise and contains much less “distraction” from business logic.
This example shows how convenient monadic “inversion of control”: transformations don’t need to check for null, they will be called only when a value is actually available.
The “do something if/when value is available” is a key mindset to start conveniently using Monads.
Note that the example above retains the original API’s intact. But it makes sense to use the approach wider and change API’s 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)));
}
Few observations:
- The code even more concise and contains nearly zero boilerplate
- All types are automatically derived. This is not always so, but in the vast majority of cases, types are derived by compiler despite weaker type inference in Java comparing to, for example, Scala
- There is no explicit error processing, instead we can focus on “happy day scenario”.
- All transformations are composing and chaining conveniently, no breaks or distractions from main business logic.
In fact, the properties above are common for all Monads.
To Throw Or Not To Throw, That Is The Question
Things not always go as we would like, and our applications live in the real world, full of suffering, errors and mistakes. Sometimes we can do something with them, sometimes not. If we can’t do anything, we would like to at least notify the caller that things went not as we anticipated.
In Java, we traditionally have two mechanisms to notify the caller about a problem:
- Return special value (usually null)
- Throw an exception
Instead of returning null we can also return Option Monad (see above), but often this is not enough as more detailed information about the error is necessary. Usually, we throw an exception in this case.
There is a problem with this approach, though. Actually, even few problems.
- Exceptions break execution flow
- Exceptions add a lot of mental overhead
Mental overhead caused by exceptions depends on the types of exceptions:
- Checked exceptions, forcing you either to take care of them right here or declare them in signature and shift the headache to the caller
- Unchecked exceptions cause the same level of issues, but you don’t have support from compiler
Don’t know which one is worse.
Here Comes Either Monad
Let’s analyze the issue for the moment. What we want to return is a some special value, which can be exactly one of two possible things: result value (in case of success) or error (in case of failure). Note that these things are mutually exclusive — if we return value, there is no need to carry error and vice versa.
The above is almost exact description of the Either Monad: any given instance contains exactly one value, and this value has one of two possible types.
The interface of Either Monad 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 rather verbose, as it’s symmetric regarding left and right values. For the narrower use case when we need to deliver success or error, it means that we need to agree on some convention — which type (first or second) will hold error and which will hold value.
The symmetric nature of Either makes it more error-prone in this case, as it’s easy to unintentionally swap error and success values in code.
While most likely this problem will be caught by the compiler, it’s better to tailor Either for this particular use case. This can be done if we fix one of the types. Obviously, it’s more convenient to fix the error type, as Java programmers are already used to having all errors and exceptions derived from a single Throwable type.
Result Monad — Either Monad Specialized for Error Handling & Propagation
So, let’s assume that all errors implement the same interface and let’s call it 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 API of Maybe 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 identical to the example above, the only change is the Monad type — Result instead of Optional. Unlike the previous example, here we have full information about error, so we can do something with that at the upper level. But still, despite full error handling, the code remains simple and focused on the business logic.
“Promise is a big word. It either makes something or breaks something.”
The next Monad I’d like to show will be the Promise Monad.
The Promise Monad represents a (potentially not yet available) value.
The Promise Monad can be used to represent, for example, the result of a request to an external service or database, file read or write, etc. Basically, it can represent anything which requires I/O and time to perform it. The Promise supports the same mindset as we’ve observed with other Monads — “do something if/when value is available”.
Note that since it’s impossible to predict whether an operation will be successful or not, it’s convenient to make Promise look like an asynchronous version of the Result Monad.
To see how it works, let's take a look example below:
...
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 bring the whole context, I’ve included both necessary interfaces, but the actually interesting part is the userTopicHandler() method. Despite suspicious simplicity, this method does the following:
- Calls TopicService and retrieve list of topics created by provided user
- When list of topics is successfully obtained, the method extracts topic ID’s and then calls ArticleService and retrieves list of articles created by user for specified topics
- Performs end-to-end error handling
Afterword
The Monads are a powerful and convenient tool. Writing code using “do when value is available” mindset requires some time to get used to, but once you start getting it, it will allow you to simplify your life a lot. It allows offloading a lot of mental overhead to the compiler and makes many errors impossible or detectable at compile time rather than at run time.