Dieser Post ist ein Teil der Reihe über funktionale Softwarearchitektur in Kotlin. Im ersten ging es um funktionale Validierung, in diesem Teil geht es um Monaden. Diese sind in Kotlin vor allem praktisch, wenn es um die Beschreibung von Abläufen in der Domäne geht, die von technischer Logik zur Ausführung dieser Abläufe getrennt werden soll – und zwar unter Verwendung von kleinen domänenspezifischen Sprachen (DSLs).

In dieser Folge geht es darum, wie Monaden überhaupt funktionieren. Kotlin hat nämlich – wie viele funktionale Sprachen auch – dafür eine spezielle Syntax, auch wenn man sie nicht unter dem „M-Wort“ in der Dokumentation findet. Sie versteckt sich hinter dem Schlüsselwort suspend.

Der Option-Typ

Wir versuchen es mit einer der einfachsten Monaden, nämlich dem Typ Option, aus getypten funktionalen Sprachen auch gelegentlich als Maybe bekannt, in Java als Optional. Er ist dafür zuständig, wenn eine Operationen manchmal ein Ergebnis liefert, manchmal aber auch nicht.

Wir bauen den Typ einfach selber, damit wir zeigen können, wie wir auf Grundlage einer Typdefinition die dazu passende Monade definieren können. Wir bauen ihn als kleine Typhierarchie mit einem Interface und den obligatorischen zwei Fällen – Some, falls ein Wert da ist und None, falls nicht:

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>

(Wer out und Nothing in Kotlin noch nicht gesehen hat: Nicht schlimm, die klären die Interaktion zwischen Generics und Typhierarchien, haben aber keine besondere inhaltliche Signifikanz.)

Um Option zu einer Monade zu machen, brauchen wir eine bind-Methode mit folgender Signatur:

sealed interface Option<out A> {
    fun <B> bind(next: (A) -> Option<B>): Option<B>
}

Die implementieren wir folgendermaßen für None und 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)
}

Das bind können wir nun benutzen, um eine Art „Poor Man‘s Exception Handling“ zu implementieren, also viele Operationen hintereinander, die jeweils schiefgehen (also None liefern) können, und wo ein Endergebnis nur herauskommt, wenn alles gutgeht. Das macht leider syntaktisch keine Freude, noch nicht mal eingefleischten Klammer-Fans wie mir:

some(5).bind { o1 ->
some(7).bind { o2 ->
  some(o1 + o2) } }

Das ist effektiv Callback Hell, und auch wenn wir eine Club-Mate-Flatrate drauflegen, werden wir damit OO-Programmierys kaum hinterm Ofen hervorlocken können, um routinemäßig monadisch zu programmieren.

In Haskell, Scala und F# beispielsweise ist jeweils syntaktischer Zucker eingebaut, mit dem wir Programme als Folge von „Statements“ schreiben können, den der Compiler dann in Aufrufe von bind und verwandten Funktionen übersetzt. In Haskell zum Beispiel sähe der Code so aus:

do o1 <- Just 5
   o2 <- Just 7
   return (o1+o2)

(In Clojure zum Beispiel können wir solchen syntaktischen Zucker auch selbst definieren.)

Coroutinen und Continuations

So schicke Syntax wie in Haskell gibt es in Kotlin auch, aber versteckt in einem Mechanismus, der Coroutinen. Die Dokumentation lässt das Lesy glauben, dass es bei Coroutinen primär um „asynchrone“ beziehungsweise nebenläufige Programmierung geht, aber dahinter steckt ein allgemeiner Mechanismus, der bei Funktionen aktiviert ist, deren Definition mit dem Schlüsselwort suspend markiert ist. Dieses versetzt den Compiler in einen anderen Modus, der daraufhin eine sogenannte CPS-Transformation durchführt.

„CPS“ steht für Continuation-Passing Style und ist eine bestimmte Art, Funktionen zu schreiben. Normalerweise schreiben wir ja Programme „verschachtelt“, indem das Ergebnis eines Funktionsaufrufs zurückgegeben wird.

f(g(h(x)))

Bei CPS geht es niemals zurück: Wenn eine Funktion fertig ist, gibt sie kein Ergebnis „zurück“, sondern ruft stattdessen eine Funktion auf, die weitermacht – eben die Continuation, englisch für „Fortsetzung“. In CPS sieht der obige geschachtelte Funktionsaufruf so aus:

h(x) { g(it) { gr -> f(it) { ... } } }

Das Programm wird also linearisiert – die Funktionsaufrufe stehen in der Reihenfolge, in der sie auch zur Laufzeit passieren. Außerdem bekommt jedes Zwischenergebnis einen Namen. Das und die geschweiften Klammern hat schonmal gewisse Ähnlichkeit zu den Aufrufen von bind weiter oben.

Warum macht der Kotlin-Compiler also eine CPS-Transformation? Motiviert ist dies tatsächlich durch „asynchrone Programmierung“, wo es darum geht zu vermeiden, dass ein JVM-Thread blockiert, weil er zum Beispiel auf I/O wartet. Warum das vermeiden? Weil JVM-Threads viel Speicher verbrauchen und eine JVM nur eine begrenzte Anzahl davon erzeugen darf. Es wäre also besser, wenn ein Thread erstmal was anderes machen könnte, wenn eine Berechnung blockieren würde und sie dann reaktiviert, wenn das I/O fertig ist. Dafür müsste das Programm speichern, wo es weitergeht.

Normalerweise weiß die JVM, wo es nach einem Methodenaufruf weitergeht, indem sie den Stack konsultiert, ein implizites Ding, welches ein Java-Programm nicht direkt manipulieren kann. In CPS jedoch ist „wie es nach einem Methodenaufruf weitergeht“ eine Continuation, also ein Objekt, das gespeichert und benutzt werden kann, um eine Berechnung zu reaktivieren.

Um damit asynchron zu programmieren, braucht das Programm Zugriff auf jenes Continuation-Objekt, und dafür gibt es eine Funktion namens suspendCoroutine, die eine übergebene Funktion aufruft mit eben der aktuellen Continuation, für die es eine Extra-Klasse in Kotlin gibt. Die Continuation wiederum hat eine Methode resume, welche die Ausführung fortführt, bis sie wieder suspendCoroutine aufruft. Das können wir nutzen, um einen Aufruf von bind einzuschmuggeln und so aus einer „ganz normalen“ suspend-Funktion ein monadisches Programm zu machen.

Das ist eine ziemliche Frickelei, aber es geht - hier ist der Code in unserer kleinen Library, die wir für den Zweck geschrieben haben. Sie stellt ein Objekt MonadDSL zur Verfügung mit einigen hilfreichen Funktionen, deren Definition wir aber gerade wegen dieser Frickeligkeit hier weglassen.

Monaden-Syntax als Kotlin-DSL

Die Abstraktionen aus unserer Library können wir benutzen, um zunächst aus einem Option<A>-Wert eine Berechnung zu machen, die wir in einer suspend-Funktion benutzen können:

sealed interface Option<out A> {
    suspend fun susp(): A = MonadDSL.susp<Option<A>, A>(this::bind)
}

Diese Methode benutzen wir nun, um eine kleine DSL zu definieren. „DSL“ heißt in Kotlin in der Regel, dass wir für einen Lambda-Ausdruck einen Satz Funktionen lokal zur Verfügung stellen, indem wir einen Funktionstyp „with receiver type“ definieren. So sieht das aus:

fun <A> optionally (block: suspend OptionDSL.() -> A): Option<A> =
	MonadDSL.effect(OptionDSL(), block)

Beim Typ OptionDSL.() -> A ist OptionDSL ein solcher „receiver type“ und er sorgt dafür, dass der Kotlin-Compiler jedem Lambda-Ausdruck, der an optionally übergeben wird, automatisch aus von der Klasse OptionDSL erbt, also dessen Funktionen benutzen kann. Außerdem bekommen sie automatisch ein suspend angehängt und werden damit vom Kotlin-Compiler CPS-transformiert.

Das Objekt OptionDSL enthält nur eine einzige Operation:

object OptionDSL {
    suspend fun  <A> pure(result: A): A = MonadDSL.pure(result) { Some(it) }
}

Größere Monaden haben hier natürlich mehr Operationen. Dies erlaubt uns nun, Programme so zu schreiben:

optionally {
  val o1 = pure(5)
  val o2 = pure(7)
  pure(o1 + o2)
}

Herauskommt Some(12) – und fertig ist die Option-Monade inklusive schöner Syntax!

Um größere Monaden und welche Rolle sie in der Software-Architektur spielen können, geht es in einem zukünftigen Post.