Java IO Monad. Reality or Fiction

Vladimir Shiryaev
7 min readJan 7, 2020

--

Monads? Again?

So much has been written and told about monads, what they are, why they are and what forms they have in our favorite programming languages. So why am I trying to raise this long-worn topic again? Or maybe I missed the hype train? Of course, we will talk about this and a little more in the following sections. But for now, trust me that this topic can be discussed from other sides or feel free to close this article if you do not believe me.

If you still continue reading, then let’s, as in the plain old articles, first go back to the start and remember what a monad is. Well, of course, everyone knows that monad is a monoid in the category of endofunctors and there’s nothing more to say. But If you are not an expert in category theory like me (I’m not an expert of course), then for you the monad is essentially an abstraction of the absence of value, in other words, it is a box that can either store a value or store its absence and which allows us to compose the computations on this value.

Any implementation of the monad must have two operations.

The binding operation that allows us to transform a value inside one monad into another monad.

<B> Monad<B> flatMap(function: Function<A, Monad<B>>)

And the return operation that allows us to get the monad from the value.

<A> Monad<A> apply(value: A)

Also, these two operations must satisfy the following 3 laws of the monad (M)

left-identity law:

M.aplly(x).flatMap(f).equals(f.apply(x))

right-identity law:

M.flatMap(M::apply).equals(M)

associativity law:

M.flatMap(f).flatMap(g).equals(M.flatMap(x -> f.apply(x).flatMap(g))

Having these operations and laws on them, we can safely build chains of lazy computations until we want to get (or not to get) the value inside our box.

Introduction to IO

IO Monad, as you probably understand by its name, is also a monad, but why IO? You are right, it encapsulates the operations of I/O like logging, read and write operation from console or maybe actions with database. But is it so important to encapsulate them? Well, it so happened that all of these funny things about work with the outside world create side effects in functions and methods that make them not referential-transparent and hence they are impure, look at the example.

Let us suppose we have some function which increments external variable in its body.

int externalVariable = 0;int addAndIncrementExtVar(int a, int b) {
final var result = a + b;
externalVariable += 1;
return result;
}

Then, the following two cases will not be equivalent and break referential transparency and prove that our method is impure.

In the first case we increment external variable twice

var sum = addAndIncrementExtVar(1, 1) + addAndIncrementExtVar(1, 1);

and in the second case we increment variable only once.

var value = addAndIncrementExtVar(1, 1);
var sum = value + value;

This is where we need our IO monad- we can wrap a method call with a potential side effect inside, thereby making this method call lazy. What will it give us? First of all, we guarantee that the method calls that we will chain using our IO Monad will be referentially transparent, and all methods with side effects will be performed only on demand, but not before that.

In some languages, such as Scala and Kotlin, there are libraries that implement the IO Monad (cats, scalaz and arrow, respectively) and have additional utilities for working with it.

Java Implementation

Okay, let’s put this all together and try to implement the IO Monad in java. First, we need to express our method call with a side effect (we call it just an “effect”). Let us express our effect as the Single Abstract Method interface- its method will return us some value of the effect execution. And since the interface is functional, we can describe our effects as lambda expressions.

public interface Effect<T> {
T run();
}

Okay, so we found out that our monad is a box, let’s try to put our effect inside.

public class IO<A> {
private final Effect<A> effect;
private IO(Effect<A> effect) {
this.effect = effect;
}
}

And we should also add a method that makes our box useful.

public A unsafeRun() {
return effect.run();
}

Now let’s implement our two operations that define the monad.

First, we implement the binding operation.

public <B> IO<B> flatMap( Function< A, IO<B>> function) {
return IO.apply(() -> function.apply(effect.run()).unsafeRun());
}

Next, we will implement an operation with which we can wrap our effect inside the monad.

public static <T> IO<T> apply(Effect<T> effect) {
return new IO<>(effect);
}

We can also easily implement the map method, cause’ It can be represented as a combination of two other operations: flatMap and apply.

public <B> IO<B> map(Function<A, B> function){
return this.flatMap(result -> IO.apply(() -> function.apply(result)));
}

Well, everything is fine, IO monad is ready !!!

Oh wait a minute, but what about the methods that return a special void type? Such a statement like System.out.println(); does not return a real type that extends from Object. Although Java has java.lang.Void type for checking the type that the method can return, we cannot use it ourselves, because It cannot be instantiated. Here we come face to face with a language restriction, because intuitively, it is normal to expect that an argument of a function like Function <T, Void> should accept a method reference like System.out::println, but unfortunately it does not work like this. Okay, let’s take advantage of the ad-hoc polymorphism in Java by using method overloading and assume that Consumer <T> is equivalent to Function <T, Void>. Then, following our assumptions, I propose to implement the map method for such a case.

public IO<Void> map(Consumer<A> function) {
return this.flatMap(result -> IO.apply(() -> {
function.accept(result);
//We can’t instantiate Void, hence we can return null only
return null;
}));
}

And now we can gracefully describe our computations.

IO.apply(() -> “abc”)
.map(String::toUpperCase)
.map(System.out::println);

Unfortunately, something went wrong again, because according to the Java Language Specification, Java SE 13 Edition in section 15.12.2.1. “Identify Potentially Applicable Methods” and to the paragraph about using lambda expressions as arguments, where the Consumer <T> type is expected, it is acceptable to use a lambda of the type Function <T, R>. That is, without explicitly casting the type of lambda, the compiler will not be able to understand what method we want to call and we will have to make the code more verbose and awkward.

IO.apply(() -> “abc”)
.map(((Function<String, String>) String::toUpperCase)
.map((Consumer<String>) System.out::println));

Therefore, we cannot elegantly handle this situation. Well, then you have to rename our poor method to something else, I suggest calling it mapToVoid, to explicitly indicate that the effect does return nothing.

By the way, it is impossible to prove 3 laws in the context of IO monad in java. Because we can`t compare two IO monads before we call the unsafeRun method on them. You can superficially check these laws by comparing the values ​​obtained from calling unsafeRun from each IO monad.

Usage

Now that we are finally done with the implementation, let’s look at its usage.

The most popular example is the work with console.

IO.apply(() -> “What is your name friend?”)
.mapToVoid(System.out::println)
.map(ignored -> System.console().readLine())
.map(name -> String.format(“Hello %s!”, name))
.mapToVoid(System.out::println);

If we run this fragment in our main method, then of course we will not get anything as a result. Because our chain is executed on demand.

Okay, add unsafeRun to the end of the chain and run it again.

Works! And now, when our effects are wrapped in the IO Monad, each of our steps (except unsafeRun) in the chain is a pure expression, because we always get a result that we expected, and what about referential transparency?

No surprises. Then the following two cases will be equivalent

In the first case we increment external variable twice

var sum = IO.apply(() -> addAndIncrementExtVar(1, 1))
.map(x -> x + addAndIncrementExtVar(1, 1))
.unsafeRun();

In the second case we increment external variable twice too!

final var value = IO.apply(() -> addAndIncrementExtVar(1, 1));var sum = value.flatMap(x -> value.map(y -> x + y))
.unsafeRun();

And as a bonus, we can add a safe way to get the value from the IO monad

public Either<Exception, A> safeRun() {
try {
return Either.right(unsafeRun());
} catch (Exception ex) {
return Either.left(ex);
}
}

Either is just a data structure that stores a value or an exception, but not both at once.

Pros and Cons

Now let’s talk about the pros and cons of IO Monad in Java:

PROS:

  • Composition. We can compose our computations
  • Purity. The IO monad itself, of course, does not make our impure functions pure, but encapsulating them as a “recipe” of an effect inside the monad and makes IO monad a pure structure.
  • Totality. Using the IO monad, we can create methods that give the result for any input
  • Referential transparency
  • Laziness. Delay our side effects for as long as we can

CONS:

  • Unlike languages ​​where the Void type (or its analogue Unit) is a real type and has a real instance, in Java you have to add methods that you could do without, and therefore the implementation of IO Monad in Java is more verbose than, for example, in Scala
  • Community support- it is, quite frankly, weak. I could find only a couple of examples of the implementation of the IO monad in Java and almost no articles explaining what it is. Of course, I believe that this is not a minus and we just need to make the community more cognizant.
  • There are no libraries that implement IO functionality as powerful as in other JVM languages ​​(Let’s write them!)

Conclusion

To keep this brief, I will say just a few words. We can hate Java as long as we want, but still there is space for implementing new approaches and methods. Of course it’s up to you, to use them in Java or use a language more suitable for this.

--

--