As a Java(script) programmer, you already know Lists and Optionals, and have used map
and flatMap
functions just fine without knowing the underlying Monad concept. However, getting to know it will make your code easier to test with fewer bugs, and make you a competent programmer on nowadays essential, harder topics like asynchronous and reactive programming.
Introduction
Programming is all about composition to tackle large problems by combining small solutions, so every programmer should know Monads by now:
Monads (in programming) are all about composition of side-effects. They have already become mainstreamm, although without recognition, in stream processing and reactive/asynchronous programming with Futures, Promises, Reactive Extensions; they will soon become mainstream in AI for (Deep) Learning through Probabilistic Programming and Automatic Differentiation. However, Monads model side-effects without actual side-effects and can look complicated. So, let’s raise our awareness to composition and side-effects separately first before we focus on composition with side-effects combined.
More: Reactive Extensions Intro Page
More: Automatic Differentiation Intro Talk
More: Probabilistic Programming Monad Talk
Function Composition in Java(script)
You already know and use function composition day by day:
static Integer length(String s) { .. } // pure
static Boolean isOdd(int n) { .. } // pure
static Boolean hasOddLength(String s) { // pure
return isOdd(length(s))
// |composition|
}
This works perfect for pure functions, that is, those without side-effects. But how do you compose impure functions, for example, those that may fail? In traditional Java(script) you “compose” as usual but introduce exceptions:
static Integer parseInt(String s) { ..throws new RuntimeException().. }
static Boolean isValidId(int n) { ..throws new RuntimeException().. }
static Boolean isValidUserId(String s) {
return isValidId(parseInt(s)) // also throws exceptions
// |---composition---|
}
This works but is dangerous, because functions with runtime exceptions don’t really compose; they lie: When using isValidUserId
nothing tells you to check for the exception, which may be thrown deep and far way from your current code. If you forget that check, your program crashes unexpectedly on some inputs.
See: Math. Function Composition Wiki
The No-Value Side-Effect With Optionals
To make runtime exceptions safer you may list all exceptions in the comments (unreliable) or switch to checked exceptions with lots of try-catch blocks (horrible). However, for the side-effect of potentially not-having a return value for some inputs there is a safer, more readable alternative: Optional
.
static Optional<Integer> parseInt(String s) { .. } // no exceptions
static Optional<Boolean> isValidId(int n) { .. } // no exceptions
static Optional<Boolean> isValidUserId(String s) { // no exceptions
// composition with manual glue code:
= parseInt(s);
var intOpt if (intOpt.isPresent()) {
return isValidId(intOpt.get());
} else {
return Optional.empty();
}
}
Even if you don’t know that Optional
is a Monad, you will surely always rewrite that last function to use flatMap
:
static Optional<Boolean> isValidUserId(String s) { // no exceptions
return parseInt(s).flatMap(isValidId))
// |--------composition----------|
}
This function cannot crash and you cannot unknowingly forget to check for the empty case, because the return type and compiler will warn you.
More: Checked vs Runtime Exceptions in Java
More: Java Optional Guide
In rough terms, a Monad is a container type with a flatMap
function, just like Optional
. This intuition suffices for now.
So, using Optional
we have turned impure functions with real side-effects (exceptions/crashes) into “impure” functions, where “side-effect” is a matter of perspective: Technically a “impure” function like isValidUserId
is still pure. It simply returns a value of type Optional<Boolean>
instead of Boolean
. However, we interpret Optional
as the “side-effect” of potentially not having a return value. This is what “side-effects without actual side-effects” means.
An “impure” function is also often called a “Monadic function”, because it returns a Monad. Different Monads represent different “side-effects”.
Compose Side-Effects with Monads
To get a deeper understanding how Monads relate to composition of functions with side-effects, we look at previous examples but switch the notation from Java to Math. Java(script) is simply too verbose and inconsistent, because you can write functions as lambdas or static methods and use different composition syntax. So, having a function like
static Boolean hasOddLength(String s) { .. } // pure
we forget its name and focus only on its signature to get its essence:
String -> Boolean
Compose Pure Functions
As before, we start with composing pure functions:
String -> Integer Integer -> Boolean
// |-pure function-| |--pure function--|
The output type of the left function matches the input of the right, so these two can be composed with a normal function composition operator:
String -> Integer `andThen` Integer -> Boolean
// result:
// String -------------------------------------> Boolean
This operator is called andThen
in Java, fmap
in Haskell, or simply compose
in some Javascript frameworks. The result is a new function that first applies the left function, then the right one; it goes directly from String
to Boolean
and is correct by construction, because there is no manual glue code that we can mess up.
See: Java’s andThen
Function
More: Function Composition Notation in Math
More: Function Composition Notation in Haskell
Pure functions, as found in purely functional languages like Haskell, can be used in other, non-pure languages such as Java and are simply the best tool for programming: They compose nicely as we have seen, they always return the same result for the same input and they don’t have side-effects. That means they don’t crash, they don’t manipulate/change any variable or any state, especially not outside the function, which makes them easily testable and simpler to reason about than every other kind of function. Furthermore, non-local computations cannot affect local ones, because there are no changes/mutation at all.
More: Pure Function Wiki
More: What is a non-local change?
Remodel Functions With Side-Effects
To compose functions with side-effects, the first step is to turn a real side-effect into a new container type: In our examples Optional
replaces an exception as the side-effect of potentially not-having a return value for some inputs. So, suppose the previous functions aren’t pure but throw runtime exceptions on some inputs:
String -> Integer Integer -> Boolean
// |with hidden exception| |with hidden exception|
We remove those exceptions and wrap/extend/embellish the return type instead:
String -> Optional<Integer> Integer -> Optional<Boolean>
// |----"impure" function----| |-----"impure" function----|
These functions now have remodelled the no-value-side-effect to the type-level. However, we now face the problem that the return type of the left function doesn’t match the input type of the right one: Normal function composition doesn’t work anymore.
Compose Monadic Functions
The next step is where the Monad concept comes into play. Since Optional
is a Monad it has a flatMap
function: We can compose (only) the left Optional
value with the right “impure” function:
<Integer> `flatMap` Integer -> Optional<Boolean>
Optional// |--- monad -----| |---"impure" function------|
But we want to compose the whole left function, not just its Optional
!? So we extend that flatMap
function to build the composition operator we actually want:
String -> Optional<Integer> `>=>` Integer -> Optional<Boolean>
// |----"impure" function----| fish |-----"impure" function----|
// result:
// String ----------------------------------------------> Optional<Boolean>
This “fish operator”, as it appears in Haskell, is a combination of a andThen
call on the left function and flatMap
on the left Optional
as follows:
(String -> Optional<Integer>).andThen(
-> opt.flatMap(Integer -> Optional<Boolean>))
opt // |----"impure" function----| |-----"impure" function----|
// result:
// String ---------------------------------------------------> Optional<Boolean>
It composes two “impure” functions into one, similar to normal function composition. The result is a function that goes directly from String
to Optional<Boolean>
and is correct by construction, because again there is no glue code that we can mess up.
See: Java’s andThen
Guide
See: Java Optional flatMap
Guide
The main takeway is that Monads allow us to model side-effects in a pure way and to compose functions with those “side-effects” in a safe, crash-free, less-error prone and easy-to-test way.
We have only seen the Optional
Monad, but there are many more Monad types, each representing a different side-effect. For example, there is the nowadays essential Future
/Promise
Monad type for asynchronous programming that represents the side-effect of not having a value yet but sometime in the future. Luckily, all Monads have in common that you can think of them as containers with a flatMap
function: If you know how to use one, you can use all the others with less difficulty.
More: Bartosz Milewski’s Category Theory Monad Introduction Using Fish
More: What is a Functor?