Monads in Kotlin
deThis post is part of the series on functional software architecture in Kotlin. The first one covered functional validation, and this part is about monads. In Kotlin, these are particularly useful when describing domain workflows that should be separated from the technical logic for executing these workflows - specifically using small domain-specific languages (DSLs).
This episode is about how monads actually work. Kotlin has - like many
functional languages - special syntax for this, even though you won‘t
find it under the „M-word“ in the documentation. It‘s hidden behind
the suspend keyword.
The Option Type
Let‘s try one of the simplest monads, namely the Option type, also
occasionally known as Maybe in
typed functional languages, or
Optional
in Java. It‘s responsible for when operations sometimes return a
result, but sometimes don‘t.
We‘ll build the type ourselves so we can show how to define the
appropriate monad based on a type definition. We build it as a small
type hierarchy with an interface and the obligatory two cases - Some,
if a value exists, and None, if not:
sealed interface Option<out A> {
companion object {
fun <A> some(value: A): Option<A> = Some(value)
fun <A> none(): Option<A> = None
fun <A> Option<A>.get(): A =
when (this) {
is None -> throw AssertionError("found None where Some was expected")
is Some -> value
}
}
}
object None : Option<Nothing>
data class Some<out A>(val value: A) : Option<A>
(For those who haven‘t seen out and Nothing in Kotlin yet: Don‘t
worry, they clarify the interaction between generics and type
hierarchies, but have no special conceptual significance.)
To make Option a monad, we need a bind
method with the
following signature:
sealed interface Option<out A> {
fun <B> bind(next: (A) -> Option<B>): Option<B>
}
We implement this interface as follows for None and Some:
object None : Option<Nothing> {
override fun <B> bind(next: (Nothing) -> Option<B>): Option<B> = None
}
data class Some<out A>(val value: A) : Option<A> {
override fun <B> bind(next: (A) -> Option<B>): Option<B> = next(this.value)
}
We can now use this bind to implement a kind of „Poor Man‘s
Exception Handling,“ that is, sequence many operations, each of which
can fail (i.e., return None), and yield a final result only if
everything succeeds. Unfortunately, this isn‘t syntactically pleasant,
not even for die-hard parenthesis fans like myself:
some(5).bind { o1 ->
some(7).bind { o2 ->
some(o1 + o2) } }
This is effectively Callback Hell, and even if we throw in a nitro cold brew flatrate, we‘re unlikely to lure OO programmers from their lair to routinely program monadically.
Haskell,
Scala, and
F#,
have syntactic built-in that lets us write programs
as a sequence of „statements,“ which the compiler then translates into
calls to bind and related functions. In Haskell, for example, the
code would look like this:
do o1 <- Just 5
o2 <- Just 7
return (o1+o2)
(In Clojure, for example, we can define such syntactic sugar ourselves.)
Coroutines and Continuations
Kotlin also has syntax as elegant as Haskell‘s, but it‘s hidden in a
mechanism called
coroutines. The
documentation makes readers believe that coroutines are primarily
about „asynchronous“ or concurrent programming, but behind it is a
general mechanism that‘s activated for functions whose definition is
marked with the suspend keyword. This puts the compiler into a
different mode, which then performs a so-called CPS
transformation.
„CPS“ stands for Continuation-Passing Style and is a particular way of writing functions. Normally we write programs „nested,“ where the result of a function call is returned.
f(g(h(x)))
With CPS, nothing ever goes back: When a function is finished, it doesn‘t „return“ a result, but instead calls a function that continues - namely the continuation. In CPS, the nested function call above looks like this:
h(x) { g(it) { gr -> f(it) { ... } } }
The program is thus linearized - the function calls appear in the order
they happen at runtime. Additionally, each intermediate result gets a
name. This, along with the curly braces, already has a certain
similarity to the calls to bind above.
So why does the Kotlin compiler perform a CPS transformation? This is actually motivated by „asynchronous programming,“ where the goal is to avoid blocking a JVM thread because it‘s waiting for I/O, for example. Why avoid this? Because JVM threads consume a lot of memory and a JVM is only allowed to create a limited number of them. It would be better if a thread could do something else when a computation would block and then reactivate it when the I/O is done. For this, the program would need to remember the place in the code where it continues.
Normally, the JVM knows where to continue after a method call by consulting the stack, an implicit thing that a Java program cannot directly manipulate. In CPS, however, „how to continue after a method call“ is a continuation, i.e., an object that can be stored and used to reactivate a computation.
To program asynchronously with this, the program needs access to that
continuation object, and there‘s a function called
suspendCoroutine
that calls a passed function with the current continuation, for which
there‘s a special
class
in Kotlin. The continuation in turn has a resume method that
continues execution until it calls suspendCoroutine again. We can
use this to sneak in a call to bind and thus turn a „completely
normal“ suspend function into a monadic program.
This is quite fiddly, but it works -
here
is the code in our small library that we wrote for this purpose. It
provides a MonadDSL object with some helpful functions, whose
definitions we omit here precisely because of this fiddliness.
Monad Syntax as a Kotlin DSL
We can use the abstractions from our library to first turn an
Option<A> value into a computation that we can use in a suspend
function:
sealed interface Option<out A> {
suspend fun susp(): A = MonadDSL.susp<Option<A>, A>(this::bind)
}
We now use this method to define a small DSL. „DSLs“ in Kotlin typically means that we provide a set of functions locally available to a lambda expression by defining a function type „with receiver type“. Here‘s what that looks like:
fun <A> optionally (block: suspend OptionDSL.() -> A): Option<A> =
MonadDSL.effect(OptionDSL(), block)
With the type OptionDSL.() -> A, OptionDSL is such a „receiver
type“ and it ensures that the Kotlin compiler automatically makes
every lambda expression passed to optionally inherit from the
OptionDSL class, which is then able to use its
functions. Additionally, they automatically get the suspend tag and
are thus CPS-transformed by the Kotlin compiler.
The OptionDSL object contains only a single operation:
object OptionDSL {
suspend fun <A> pure(result: A): A = MonadDSL.pure(result) { Some(it) }
}
Larger monads naturally have more operations. This allows us to write programs like this:
optionally {
val o1 = pure(5)
val o2 = pure(7)
pure(o1 + o2)
}
The result is Some(12) - and the Option monad is complete,
including beautiful syntax!
A future post will cover larger monads and what role they can play in software architecture.