explaining monads for non functional programmers
I am making this post mostly out of frustration, when learning functional programming a lot of people encounter the monad, which as a concept is suprisingly simple once you understand it. However I also feel like most people are absolutely awfull at explaining it.
In this post I tried to explain it from the perspective of an average
developer, that's why I will be writing it in java to hopefully make
it more clear due to a familier syntax.
Before I continue on, if you ever feel what I am telling you is too simple to be true, then it probably is that simple and true. Monads are very simple consturctions that we named since in some languages knowing something is a monad allows you to reuse a lot of code. So yes if you ever think "no way it's just that" then it probably is just that.
1. prerequisite, java.util.function
java has an object called Function, Function is just an object
representing a function, meaning that Function<Integer, String> is an
object containing a function that takes in an Integer and returns a
String. To call said function we need to use .apply(), so say func1 is
of type Function<Integer, String>, we can then call func1.apply(5) to
get a String.
2. conceptually
The concept of a monad for programmers can be boiled down heavily, namely it's an object that wraps a type while supporting 2 function/methods.
- bind
- wrap
wrap will just be our constructor in this case, since it well, takes in the value we want to wrap, and it wraps it in our monad. Bind on the other hand is a bit harder to explain, so for convenience sake I will just give the method signature as psuedo-java code:
class Monad<A> {
public <B> Monad<B> bind(Function<A, Monad<B>> func);
}
so it takes in a function that takes in A, and return Monad<B>, and then returns a Monad<B> from our Monad<A>. If this doesn't make sense, don't worry, we can implement an example that can hopefully shine some more light onto it.
3. Identity Monad
the "Identity Monad" is the simplest monad we can create, it does nothing but wrap the value. That it is, no magic, no nothing, just a object that functions as a wrapper for another object.
public class IdentityMonad<T> {
public T value;
public IdentityMonad(T value) {
this.value = value;
}
public <R> IdentityMonad<R> bind(Function<T, IdentityMonad<R>> f) {
return f.apply(value);
}
}
This is our entire implementation, we store a generic value T, our constructor takes in a value T and wraps it in our object, and bind just applies a function to the inside object and returns its result.
Now for some example code that uses this object:
void main() {
var originalValue = 5;
var monadOriginal = new IdentityMonad<Integer>(originalValue);
System.out.println(monadOriginal.value);
var monadNew = monadOriginal.bind((v) -> new IdentityMonad<Integer>(v + 5));
System.out.println(monadNew.value);
// will print:
// 5
// 10
}
Now ofcourse this doesn't seem all too usefull, and that's because it isn't, the IdentityMonad (in most cases) functions more as something to use as an example. When learning about monads it can help you to understand what the structure of a monad is, and hopefully show you that the structure is frankly very simple. It's just an object that holds a type, and allows u to apply a function to that type returning a new wrapper.
So now for something a bit more complicated, a State Monad, or as how I will be calling it, a composition monad.
4. Composition Monad
4.1. prerequisite
composing functions just means feeding ones result to the others. So for example, when we want to compose function1 and function2 we just write function1(function2(somevalue)). Often times we want our result to also be a function, which we can do with a java lambda.
var function1 = ...;
var function2 = ...;
var composedFunction = (someValue) -> function1.apply(function2.apply(someValue));
4.2. what we will be making
To make a composition monad, we just want to wrap a Function. Instead of taking in 1 generic, we take in 2, one representing the functions input, the other the functions output. (I and O respectively)
public class CompositionMonad<I, O> {
public Function<I, O> func;
public CompositionMonad(Function<I, O> f) {
func = f;
}
public <R> CompositionMonad<I, R> bind(Function<O, R> func) {
// TODO
}
public O apply(I input) { return func.apply(input); }
}
generic I is our input, generic O is our ouput. our constructor wraps
the type like you would expect, and for convience sake I added an
apply method so we don't have to call .func.apply(). But how do we
compose our functions, well simple, just like how we saw in our
4.1. We create a lambda, lambda has an input, we feed that
input to our this.func, then we feed this.func's output to the
parameter func.
public class CompositionMonad<I, O> {
public Function<I, O> func;
public CompositionMonad(Function<I, O> f) {
func = f;
}
public <R> CompositionMonad<I, R> bind(Function<O, R> func) {
var result = new
CompositionMonad<I, R>
((someI) -> func.apply(this.func.apply(someI)));
return result;
}
public O apply(I input) { return func.apply(input); }
}
That's is, now for an example on how to use this object:
var someComposition =
new CompositionMonad<Integer, Integer>((a) -> a + 5)
.bind((a) -> a * 2)
.bind((a) -> a.toString());
var number = 10;
String result = someComposition.apply(number);
if (result instanceof String) System.out.println("It's a string !");
System.out.println(result);
// will print:
// "It's a string !"
// "30"
So in our first variable, we make a function that takes in an int and returns int + 5. then we bind it with a function from int to int * 2. then we bind it to a function that converts the into a string.
This results is in a composed function, that takes in an int, adds 5 to it, then doubles it and returns the string representation of said int.
Now in java it might be seen as an odd construction, since passing functions as data is a lot less common, but in many functional programming language having a nice way to compose these functions can prove a lot more usefull, especially since u can just produce a composition based on the programs flow. Point is that this gives us an easy way to pass functions around as data and combine 2 functions as if it was just data, kind of like addition.
5. afterword
I hope this explanation was helpfull to anyone. Ofcourse there is always the possibility that this might come over as very confusing to people but clear to me, that is the curse that comes with understanding monads.
I would also like to specify I left out some details to monads, like for example typically u have a set of "monadic laws", for example certain computations that should always be equivalent. I chose to not cover this since in a lot of cases these are either self explanitory or the person implementing the monad doesn't care for one reason or another.
If you have any questions, while I can't promise I'll always be able to answer your question, feel free to send me an email <luniya at slugcats dot moe>
If you noticed I made some mistake or some oversight, feel free to message send me an email as well!