Beautiful World of Monads
Because Monads remained out of the focus of other past articles. I don’t know why this happens, but I’ll try to fill this gap.
Join the DZone community and get the full member experience.
Join For FreeLet me start with a disclaimer. The explanation below is no way pretends to be precise or absolutely accurate from Functional Programming. Instead, I’m focusing on the 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 are overwhelming amounts of information, but very little of it is understandable for the average Java developer with an almost exclusively imperative background. These days the situation is slowly changing. There are a lot of articles that explain, for example, basic FP concepts and how they apply to Java. Or articles explaining how to use Java streams properly. But Monads 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, some methods allow wrap value into Monad.
- Monad implements “Inversion of Control” for the value contained inside. To achieve this Monad provides methods that accept functions. These functions take the value of the same type as stored in Monad and return transformed value. The transformed value is wrapped into the same kind of Monad as 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 giving access to value is not a big difference. In fact, this enables Monad to retain full control over how and when to apply the transformation function. When you call 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 a value that 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 many 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 the value is missing. As a consequence Optional usage is often limited to represent return potentially missing value, although this is only a small part of the potential usages.
The purpose of the Maybe Monad is to represent a value that potentially can be missing. Traditionally this role in Java is reserved for null. Unfortunately, this causes a lot of 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:
x
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 at how Option Monad changes this (with one static import for brevity):
xxxxxxxxxx
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 the 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:
xxxxxxxxxx
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 are no explicit error processing, instead, we can focus on the “happy day scenario”.
- All transformations are composing and chaining conveniently, with 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 going as we would like and our applications living 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 a few problems.
- Exceptions break execution flow
- Exceptions add a lot of mental overhead
The mental overhead caused by exceptions depends on types of exceptions:
- Checked exceptions forcing you either to take care of them right here or declare them in signature and shift headache to the caller
- Unchecked exceptions cause the same level of issues but you don’t have support from the 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 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.
Above is almost an exact description of 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:
xxxxxxxxxx
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 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 and 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:
xxxxxxxxxx
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 Maybe Monad.
Using this Monad we can rewrite the previous example:
xxxxxxxxxx
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 kind of Monad — Result instead of Optional. Unlike the previous examples here we have full information about the error, so we can do something with that at the upper level. But still, despite full error handling 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.
Must admit that I’ve not found authoritative answer if Promise is a monad or not. Different authors have different opinion in regard to it. I’m looking at it from purely pragmatic point of view: it looks and behaves a lot like other monads, so I consider them a monad.
The Promise Monad represents a (potentially not yet available) value. In some sense, it’s very similar to Maybe Monad.
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 that 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 the value is available”.
Note that since it’s impossible to predict if an operation will be successful or not, it’s convenient to make Promise represent not value itself but Result with value inside.
To see how it works, let's take a look example below:
xxxxxxxxxx
...
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 a list of topics created by the provided user
- When the list of topics is successfully obtained, the method extracts topic ID’s and then calls ArticleService and retrieves a list of articles created by the user for specified topics
- Performs end-to-end error handling
Afterword
The Monads are an extremely powerful and convenient tool. Writing code using a “do when the 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 us to offload a lot of mental overhead to the compiler and make many errors impossible or detectable at compile time rather than at the run time.
Published at DZone with permission of Sergiy Yevtushenko. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments