Before you start reading: This question is not about understanding monads, but it is about identifying the limitations of the Java type system which prevents the declaration of a Monad
interface.
In my effort to understand monads I read this SO-answer by Eric Lippert on a question which asks about a simple explanation of monads. There, he also lists the operations which can be executed on a monad:
- That there is a way to take a value of an unamplified type and turn it into a value of the amplified type.
- That there is a way to transform operations on the unamplified type into operations on the amplified type that obeys the rules of functional composition mentioned before
- That there is usually a way to get the unamplified type back out of the amplified type. (This last point isn't strictly necessary for a monad but it is frequently the case that such an operation exists.)
After reading more about monads, I identified the first operation as the return
function and the second operation as the bind
function. I was not able to find a commonly used name for the third operation, so I will just call it the unbox
function.
To better understand monads, I went ahead and tried to declare a generic Monad
interface in Java. For this, I first looked at the signatures of the three functions above. For the Monad M
, it looks like this:
return :: T1 -> M<T1>
bind :: M<T1> -> (T1 -> M<T2>) -> M<T2>
unbox :: M<T1> -> T1
The return
function is not executed on an instance of M
, so it does not belong into the Monad
interface. Instead, it will be implemented as a constructor or factory method.
Also for now, I omit the unbox
function from the interface declaration, since it is not required. There will be different implementations of this function for the different implementations of the interface.
Thus, the Monad
interface only contains the bind
function.
Let's try to declare the interface:
public interface Monad {
Monad bind();
}
There are two flaws:
- The
bind
function should return the concrete implementation, however it does only return the interface type. This is a problem, since we have the unbox operations declared on the concrete subtypes. I will refer to this as problem 1. - The
bind
function should retrieve a function as a parameter. We will address this later.
Using the concrete type in the interface declaration
This addresses problem 1: If my understanding of monads is correct, then the bind
function always returns a new monad of the same concrete type as the monad where it was called on. So, if I have an implementation of the Monad
interface called M
, then M.bind
will return another M
but not a Monad
. I can implement this using generics:
public interface Monad<M extends Monad<M>> {
M bind();
}
public class MonadImpl<M extends MonadImpl<M>> implements Monad<M> {
@Override
public M bind() { /* do stuff and return an instance of M */ }
}
At first, this seems to work, however there are at least two flaws with this:
This breaks down as soon as an implementing class does not provide itself but another implementation of the
Monad
interface as the type parameterM
, because then thebind
method will return the wrong type. For example thepublic class FaultyMonad<M extends MonadImpl<M>> implements Monad<M> { ... }
will return an instance of
MonadImpl
where it should return an instance ofFaultyMonad
. However, we can specify this restriction in the documentation and consider such an implementation as a programmer error.The second flaw is more difficult to resolve. I will call it problem 2: When I try to instantiate the class
MonadImpl
I need to provide the type ofM
. Lets try this:new MonadImpl<MonadImpl<MonadImpl<MonadImpl<MonadImpl< ... >>>>>()
To get a valid type declaration, this has to go on infinitely. Here is another attempt:
public static <M extends MonadImpl<M>> MonadImpl<M> create() { return new MonadImpl<M>(); }
While this seems to work, we just defered the problem to the called. Here is the only usage of that function that works for me:
public void createAndUseMonad() { MonadImpl<?> monad = create(); // use monad }
which essentially boils down to
MonadImpl<?> monad = new MonadImpl<>();
but this is clearly not what we want.
Using a type in its own declaration with shifted type parameters
Now, let's add the function parameter to the bind
function: As described above, the signature of the bind
function looks like this: T1 -> M<T2>
. In Java, this is the type Function<T1, M<T2>>
. Here is the first attempt to declare the interface with the parameter:
public interface Monad<T1, M extends Monad<?, ?>> {
M bind(Function<T1, M> function);
}
We have to add the type T1
as generic type parameter to the interface declaration, so we can use it in the function signature. The first ?
is the T1
of the returned monad of type M
. To replace it with T2
, we have to add T2
itself as a generic type parameter:
public interface Monad<T1, M extends Monad<T2, ?, ?>,
T2> {
M bind(Function<T1, M> function);
}
Now, we get another problem. We added a third type parameter to the Monad
interface, so we had to add a new ?
to the usage of it. We will ignore the new ?
for now to investigate the now first ?
. It is the M
of the returned monad of type M
. Let's try to remove this ?
by renaming M
to M1
and by introducing another M2
:
public interface Monad<T1, M1 extends Monad<T2, M2, ?, ?>,
T2, M2 extends Monad< ?, ?, ?, ?>> {
M1 bind(Function<T1, M1> function);
}
Introducing another T3
results in:
public interface Monad<T1, M1 extends Monad<T2, M2, T3, ?, ?>,
T2, M2 extends Monad<T3, ?, ?, ?, ?>,
T3> {
M1 bind(Function<T1, M1> function);
}
and introducing another M3
results in:
public interface Monad<T1, M1 extends Monad<T2, M2, T3, M3, ?, ?>,
T2, M2 extends Monad<T3, M3, ?, ?, ?, ?>,
T3, M3 extends Monad< ?, ?, ?, ?, ?, ?>> {
M1 bind(Function<T1, M1> function);
}
We see that this will go on forever if we try to resolve all ?
. This is problem 3.
Summing it all up
We identified three problems:
- Using the concrete type in the declaration of the abstract type.
- Instantiating a type which receives itself as generic type parameter.
- Declaring a type which uses itself in its declaration with shifted type parameters.
The question is: What is the feature that is missing in the Java type system? Since there are languages which work with monads, these languages have to somehow declare the Monad
type. How do these other languages declare the Monad
type? I was not able to find information about this. I only find information about the declaration of concrete monads, like the Maybe
monad.
Did I miss anything? Can I properly solve one of these problems with the Java type system? If I cannot solve problem 2 with the Java type system, is there a reason why Java does not warn me about the not instantiable type declaration?
As already stated, this question is not about understanding monads. If my understanding of monads is wrong, you might give a hint about it, but don't attempt to give an explanation. If my understanding of monads is wrong the described problems remain.
This question is also not about whether it is possible to declare the Monad
interface in Java. This question already received an answer by Eric Lippert in his SO-answer linked above: It is not. This question is about what exactly is the limitation that prevents me from doing this. Eric Lippert refers to this as higher types, but I can't get my head around them.
Most OOP languages do not have a rich enough type system to represent the monad pattern itself directly; you need a type system that supports types that are higher types than generic types. So I wouldn't try to do that. Rather, I would implement generic types that represent each monad, and implement methods that represent the three operations you need: turning a value into an amplified value, turning an amplified value into a value, and transforming a function on unamplified values into a function on amplified values.