Monaden sind nicht gruselig, sondern ein Design Pattern
Wer schon einmal fünf Minuten in der Nähe eines Fans der funktionaler Programmierung verbracht hat, hat das Wort „Monaden” sicherlich schon einmal gehört.
Wer danach weitere fünf Minuten ausgehalten hat, hat vermutlich auch einen (vermutlich erfolglosen) Versuch miterlebt, zu erklären, was eine Monade ist.
Dabei ist das Konzept eigentlich nicht kompliziert, sondern nur schwer erklärbar.
In diesem Artikel werde ich als Beispiel einen Result- und einen Option-Typ in PHP implementieren und einige Anwendungsbeispiele davon zeigen. Anhand dessen leiten wir uns danach ein Verständnis ab, was ein Monade sein soll.
Der Result-Typ
Ein Result enthält zwei mögliche Werte: Entweder, eine Berechnung hat geklappt und es enthält das Ergebnis, oder ein Fehler ist aufgetreten, und es enthält für den Fehler relevante Daten. Mein Result für PHP sieht etwa so aus:
/**
* @template T
* @template E
*/
class Result {
/** @var T */
private mixed $value;
/** @var E */
private mixed $errorValue;
/**
* @param bool $isOk
* @param T|E $value
*/
private function __construct(private readonly bool $isOk, mixed $value) {
if ($this->isOk) {
$this->value = $value;
} else {
$this->errorValue = $value;
}
}
// ...
}
(Da PHP keine Generics unterstützt, schaffen wir uns mit PHPDocs Abhilfe)
Wir haben also einen Status isOk und je nach diesem Status ein T in $value oder ein E in $errorValue.
Um diese Resultate zu instanziieren, bieten wir zwei Funktionen ok() und error() an:
class Result {
// ...
/**
* @param T $value
* @return self<T, E>
*/
public static function ok(mixed $value): self
{
return new self(true, $value);
}
/**
* @param E $value
* @return self<T, E>
*/
public static function error(mixed $value): self {
return new self(false, $value);
}
// ...
}
Das reicht uns soweit schon, um das Result aus einer Funktion zurückzugeben, beispielsweise:
/**
* @return Result<int, string>
*/
public static function doStuff(/*...*/): Result {
// ...
// hier irgendwo wird ein bool $condition, ein int $successValue oder ein string $errorValue definiert
if ($condition) {
return Result::ok($successValue);
}
return Result::error($errorValue);
}
Hier zeigt sich auch schon der erste Vorteil des Result-Typs: Im Typ ist direkt die Bedeutung des Ergebnisses kodiert. Dank PHPs Union Types hätte doStuff auch einfach int|string zurückgeben können - dann müsste aber per Dokumentation klargestellt werden, dass int den erfolgreichen und string den Fehlerfall beschreibt.
Die Bind-Funktion und Railway-Oriented Programming
Das letzte fehlende Puzzlestück für unser Result ist eine sogenannte Bind-Funktion - hier wie in Rust "and then" genannt:
class Result {
// ...
/**
* @template N
* @param callable(T): Result<N, E> $cb
* @return self<N, E>
*/
public function andThen(callable $cb): self {
if ($this->isOk) {
return $cb($this->value);
}
return $this;
}
// ...
}
Wenn unsere vorherige Berechnung erfolgreich war (-> isOk ist true), dann wird das übergebene Callback auf dieser Funktion ausgeführt und gibt ein neues Result, potenziell mit einem neuen Ergebnistypen (aber dem gleichen Fehlertypen) zurück.
Diese Funktion erlaubt es uns, Results zu verketten und mit ihrem Ergebnis weiterzuarbeiten. Sagen wir zum Beispiel, wir haben ein Produkt, für das wir ein Bild laden und verknüpfen wollen. Dafür haben wir zwei Helferfunktionen parseAndSaveFile und setFilenameAndSave, die wir dann per andThen verketten:
class Product {
// ...
/**
* @return Result<string, array> The file name on success, the error response otherwise.
*/
private static function parseAndSaveFile(string $file): Result
{
// ...
}
/**
* @return Result<true, array> True if everything goes right, the error response otherwise.
*/
private function setFilenameAndSave(string $filename): Result
{
// ...
}
/**
* @return Result<true, array> True on success, the error response otherwise.
*/
public function parseAndSaveImage(string $file): Result
{
$filename = $this->name . '_' . time();
return self::parseAndSaveFile($file)
->andThen(fn (string $filename) => $this->setFilenameAndSave($filename));
}
// ...
}
So lassen sich theoretisch unendlich viele Funktionen verketten - sobald bei einer ein Fehler auftritt und ein Error-Result zurückgegeben wird, wird die Berechnung abgebrochen und der Error beibehalten.
Eine beliebte Analogie dafür ist, dass sich das Programm auf zwei Schienen bewegen kann: Der "Happy path", bei dem alles geklappt hat, und der "Error path", wenn ein Fehler aufgetreten ist. Die andThen-Funktion stellt hier eine Weiche dar: Programme auf dem Error path werden kommentarlos durchgewunken, Programme auf dem Happy path biegen je nach Ergebnis auf den Error path ab oder bleiben auf dem Happy path.
(Monaden zu erklären, ohne Züge und Schienen zu erwähnen, wäre wie Objektorientierung und Vererbung zu erklären, ohne Tiere oder Autos zu erwähnen)
Ende der Pipeline
Jetzt haben wir unsere Berechnung abgeschlossen und wollen unser Resultat aus dem Result-Typen herausziehen.
Das ist der eine Bereich, in dem sich zeigt, dass PHP nicht für diese Art von Datentyp ausgelegt ist;
In Rust geht das zum Beispiel recht elegant:
fn example() -> Result<u8, String> { /* ... */ }
fn example2() {
let output = match example() {
Ok(output) => output,
Err(error) => {
println!("{}", error);
return 0
}
}
println!("{}", output);
}
Da wir in PHPs match nicht auf den Wert des Results zugreifen können, müssen wir stattdessen tricksen:
function example2() {
$result = example();
$output = match ($result->isOk()) {
true => $result->getValueOrError();
false => (static function(string $error) {
echo $error;
return 0;
})($result->getValueOrError())
}
echo $output;
}
// in Result.php
class Result {
// ...
/**
* @return T|E
*/
public function getValueOrError(): mixed {
if ($this->isOk) {
return $this->value;
}
return $this->errorValue;
}
public function isOk(): bool {
return $this->isOk;
}
}
Alternativ lassen sich je nach Use Case auch Helferfunktionen verwenden - einige Beispiele dafür lassen sich in [Rusts eingebautem Result-Typ](https://doc.rust-lang.org/std/result/enum.Result.html) finden.
Der Option-Typ
Option-Typen (oder Optional in Java, Maybe in Haskell - wäre ja langweilig, wenn wir uns auf einen Namen einigen könnten) verhalten sich recht ähnlich zu Result-Typen, nur halten sie im Fehlerfall keinen Wert vor. Meine Option sieht entsprechend wie folgt aus:
/**
* @template T
*/
class Option {
private function __construct(
private readonly mixed $value
) {};
/**
* @param T $value
* @return Option<T>
*/
public static function some(mixed $value): self
{
return new self($value);
}
/**
* @return Option<T>
*/
public static function none(): self {
return new self(null);
}
/**
* @template N
* @param callable(T): N $cb
* @return Option<N>
*/
public function andThen(callable $cb): self {
if ($this->value !== null) {
return $cb($this->value);
}
return $this;
}
}
Die restliche Logik und Anwendungsbeispiele sind als Übung dem Leser überlassen.
Was ist denn jetzt eine Monade?
Um unsere eigentliche Frage zu beantworten, werfen wir einen Blick zurück darauf, was wir gemacht haben:
- Wir haben einen Wrapper-Typ entwickelt, der neben dem Ergebnis einer Berechnung auch kodiert, ob die Berechnung erfolgreich war.
- Wir haben eine Möglichkeit, einen Wert in diesen Wrapper-Typ zu überführen (unser Result::ok() bzw. Option::some()).
- Wir haben eine Funktion, um mit diesem Ergebnis weiterzuarbeiten, wenn wir uns auf dem Happy path befinden, und ansonsten abzubrechen (unsere Bind-Funktion).
Das ist eine Monade. Nicht mehr und nicht weniger.
Die beiden Beispielfälle, die ich demonstriert habe, stellen auch die verbreitetsten Anwendungen dieses Patterns dar und sind beide darauf ausgelegt, ähnliche existierende Mechanismen zu ersetzen:
- Options sind eine elegantere Abstraktion für NULL, die uns zwingt, explizit darauf zu prüfen, ob ein Wert existiert oder nicht. Wer noch nie ein Produktivsystem gecrasht hat, weil er einen Null-Check vergessen hat, hat entweder noch nie produktiven Code geschrieben oder ausschließlich mit solchen Abstraktionen gearbeitet.
- Results sind ein Gegenstück zu Exceptions - Wo Exceptions den Programmfluss (und Lesefluss im Code) unterbrechen und oft nicht transparent sind, macht ein Result-Typ von vornherein klar, dass eine Berechnung schiefgehen kann und zwingt den Aufrufer, mit diesem Fehlerfall umzugehen.
In beiden Fällen ist das Monaden-Pattern praktisch, um sicherzustellen, dass Fehlerfälle klar ersichtlich sind und korrekt behandelt werden.
Christina Reichel
18.09.2025
