Kapitel 09: Monadische Zwiebeln
Pointed-Funktor-Fabrik
Bevor wir weitermachen, muss ich ein Geständnis machen: Ich war nicht ganz ehrlich bezüglich der of-Methode, die wir in allen unseren Typen verwenden. Tatsächlich dient sie nicht dazu, das new-Schlüsselwort zu vermeiden, sondern Werte in einen sogenannten Standard-Minimal-Kontext zu setzen. Ja, of ersetzt keinen Konstruktor - es ist Teil einer wichtigen Schnittstelle namens Pointed.
Ein Pointed Functor ist ein Funktor mit einer
of-Methode
Hierbei wesentlich ist die Fähigkeit, beliebige Werte in unseren Typ einzusetzen und direkt mit map weiterarbeiten zu können.
IO.of('tetris').map(concat(' master'));
// IO('tetris master')
Maybe.of(1336).map(add(1));
// Maybe(1337)
Task.of([{ id: 2 }, { id: 3 }]).map(map(prop('id')));
// Task([2,3])
Either.of('The past, present and future walk into a bar...').map(concat('it was tense.'));
// Right('The past, present and future walk into a bar...it was tense.')Erinnern wir uns: IO und Task erwarten eine Funktion als Konstruktorargument. Maybe und Either hingegen nicht. Diese Schnittstelle ermöglicht eine einheitliche Methode, Werte in Funktoren zu platzieren - ohne spezifische Konstruktorlogik. Der Begriff „Standard-Minimal-Kontext“ ist zwar vage, trifft aber den Kern: Wir möchten Werte in den Typ heben und wie gewohnt map verwenden.
Eine wichtige Korrektur: Left.of ergibt schlicht keinen Sinn. Jeder Funktor benötigt genau einen Weg, Werte aufzunehmen. Bei Either ist dies new Right(x). Wir definieren of über Right, weil wenn ein Typ map kann, sollte er es auch tun. Wie die Beispiele zeigen, widerspricht Left dieser Intuition.
Sie kennen vielleicht Funktionen wie pure, point, unit oder return. Dies sind internationale Synonyme für unsere mysteriöse of-Methode. Sie wird wichtig, wenn wir Monaden verwenden, denn hier müssen wir Werte manuell zurück in den Typ setzen.
Um new zu vermeiden, gibt es JavaScript-Tricks und Bibliotheken. Nutzen wir verantwortungsvoll of! Ich empfehle Funktor-Instanzen aus folktale, ramda oder fantasy-land, da sie korrekte of-Methoden und new-freie Konstruktoren bieten.
Metaphernmix

Monaden sind wie Zwiebeln (neben Weltraum-Burritos, falls Sie den Gerüchten glauben). Ein Beispiel:
const fs = require('fs');
// readFile :: String -> IO String
const readFile = filename => new IO(() => fs.readFileSync(filename, 'utf-8'));
// print :: String -> IO String
const print = x => new IO(() => {
console.log(x);
return x;
});
// cat :: String -> IO (IO String)
const cat = compose(map(print), readFile);
cat('.git/config');
// IO(IO('[core]\nrepositoryformatversion = 0\n'))Hier haben wir ein IO innerhalb eines anderen IO - weil print während map ein zweites IO erzeugte. Um weiterzuarbeiten, benötigen wir map(map(f)) und doppeltes unsafePerformIO().
// cat :: String -> IO (IO String)
const cat = compose(map(print), readFile);
// catFirstChar :: String -> IO (IO String)
const catFirstChar = compose(map(map(head)), cat);
catFirstChar('.git/config');
// IO(IO('['))Zwar zeigen die zwei Effekte Bereitschaft zur Anwendung, aber dieser API-Aufbau ist unbequem. Betrachten wir ein weiteres Szenario:
// safeProp :: Key -> {Key: a} -> Maybe a
const safeProp = curry((x, obj) => Maybe.of(obj[x]));
// safeHead :: [a] -> Maybe a
const safeHead = safeProp(0);
// firstAddressStreet :: User -> Maybe (Maybe (Maybe Street))
const firstAddressStreet = compose(
map(map(safeProp('street'))),
map(safeHead),
safeProp('addresses'),
);
firstAddressStreet({
addresses: [{ street: { name: 'Mulburry', number: 8402 }, postcode: 'WC2N' }],
});
// Maybe(Maybe(Maybe({name: 'Mulburry', number: 8402})))Erneut verschachtelte Funktoren: Drei Fehlermöglichkeiten sind informativ, aber dreimaliges map-Aufrufen ist aufdringlich. Dieses Muster erfordert monadische Magie.
Monaden gleichen Zwiebeln, weil uns die Tränen kommen, wenn wir verschachtelte Schichten per map abtragen. Mit join trocknen wir die Tränen.
const mmo = Maybe.of(Maybe.of('nunchucks'));
// Maybe(Maybe('nunchucks'))
mmo.join();
// Maybe('nunchucks')
const ioio = IO.of(IO.of('pizza'));
// IO(IO('pizza'))
ioio.join();
// IO('pizza')
const ttt = Task.of(Task.of(Task.of('sewers')));
// Task(Task(Task('sewers')));
ttt.join();
// Task(Task('sewers'))Bei zwei Schichten gleichen Typs vereinen wir sie mit join. Diese Verschmelzung macht Monaden aus. Präzisieren wir die Definition:
Monaden sind pointed Funktoren, die flach gemacht werden können
Jeder Funktor mit join- und of-Methode, der bestimmte Gesetze erfüllt, ist eine Monade. Implementieren wir join für Maybe:
Maybe.prototype.join = function join() {
return this.isNothing() ? Maybe.of(null) : this.$value;
};So einfach wie embryonale Resorption. Bei Maybe(Maybe(x)) entfernt .value die überflüssige Schicht. Andernfalls bleibt das ursprüngliche Maybe erhalten.
Mit unserer join-Methode verzaubern wir das firstAddressStreet-Beispiel:
// join :: Monad m => m (m a) -> m a
const join = mma => mma.join();
// firstAddressStreet :: User -> Maybe Street
const firstAddressStreet = compose(
join,
map(safeProp('street')),
join,
map(safeHead), safeProp('addresses'),
);
firstAddressStreet({
addresses: [{ street: { name: 'Mulburry', number: 8402 }, postcode: 'WC2N' }],
});
// Maybe({name: 'Mulburry', number: 8402})join hält verschachtelte Maybe-Instanzen im Griff. Testen wir dies mit IO:
IO.prototype.join = function() {
const $ = this;
return new IO(() => $.unsafePerformIO().unsafePerformIO());
};Wir führen die IO-Schichten sequenziell aus: Äußere zuerst, dann innere. Die funktionale Reinheit bleibt gewahrt - wir reduzieren nur Verpackungslagen.
// log :: a -> IO a
const log = x => new IO(() => {
console.log(x);
return x;
});
// setStyle :: Selector -> CSSProps -> IO DOM
const setStyle =
curry((sel, props) => new IO(() => jQuery(sel).css(props)));
// getItem :: String -> IO String
const getItem = key => new IO(() => localStorage.getItem(key));
// applyPreferences :: String -> IO DOM
const applyPreferences = compose(
join,
map(setStyle('#main')),
join,
map(log),
map(JSON.parse),
getItem,
);
applyPreferences('preferences').unsafePerformIO();
// Object {backgroundColor: "green"}
// <div style="background-color: 'green'"/>Da getItem ein IO String zurückgibt, parsen wir es mit map. Weil log und setStyle eigene IOs erzeugen, benötigen wir join.
Meine Kette trifft meine Brust

Es fällt auf: Nach map folgt oft join. Fassen wir dies in einer chain-Funktion zusammen.
// chain :: Monad m => (a -> m b) -> m a -> m b
const chain = curry((f, m) => m.map(f).join());
// or
// chain :: Monad m => (a -> m b) -> m a -> m b
const chain = f => compose(join, map(f));Diese map/join-Kombination nennen wir chain (auch flatMap oder >>=). Mit chain refaktorisieren wir unsere Beispiele:
// map/join
const firstAddressStreet = compose(
join,
map(safeProp('street')),
join,
map(safeHead),
safeProp('addresses'),
);
// chain
const firstAddressStreet = compose(
chain(safeProp('street')),
chain(safeHead),
safeProp('addresses'),
);
// map/join
const applyPreferences = compose(
join,
map(setStyle('#main')),
join,
map(log),
map(JSON.parse),
getItem,
);
// chain
const applyPreferences = compose(
chain(setStyle('#main')),
chain(log),
map(JSON.parse),
getItem,
);Ersetzung durch chain vereinfacht den Code. Stilsache? Nein - chain ermöglicht effektvolles Verschachteln und funktionale Variablenzuweisung.
// getJSON :: Url -> Params -> Task JSON
getJSON('/authenticate', { username: 'stale', password: 'crackers' })
.chain(user => getJSON('/friends', { user_id: user.id }));
// Task([{name: 'Seimith', id: 14}, {name: 'Ric', id: 39}]);
// querySelector :: Selector -> IO DOM
querySelector('input.username')
.chain(({ value: uname }) =>
querySelector('input.email')
.chain(({ value: email }) => IO.of(`Welcome ${uname} prepare for spam at ${email}`))
);
// IO('Welcome Olivia prepare for spam at olivia@tremorcontrol.net');
Maybe.of(3)
.chain(three => Maybe.of(2).map(add(three)));
// Maybe(5);
Maybe.of(null)
.chain(safeProp('address'))
.chain(safeProp('street'));
// Maybe(null);Zwar könnten wir compose verwenden, aber chain erlaubt explizite Zuweisungen. Die Infix-Version lässt sich automatisch aus map und join ableiten: t.prototype.chain = function(f) { return this.map(f).join(); }. Interessant: Aus chain können wir map und join rekonstruieren. Fantasylands Spezifikation beleuchtet diese Zusammenhänge.
In den Beispielen ketten wir asynchrone Task-Operationen: Erst Benutzerabruf, dann Freundesliste. chain verhindert Task(Task([Friend])).
Bei der Willkommensnachricht-Erstellung haben wir Zugriff auf uname und email - funktionale Zuweisung par excellence. IO vertraut uns seinen Wert an, den wir via IO.of korrekt zurückgeben.
querySelector('input.username').chain(({ value: uname }) =>
querySelector('input.email').map(({ value: email }) =>
`Welcome ${uname} prepare for spam at ${email}`));
// IO('Welcome Olivia prepare for spam at olivia@tremorcontrol.net');Die Maybe-Beispiele zeigen: Bei null bricht chain die Berechnung sofort ab.
Keine Sorge bei Verständnisschwierigkeiten! Experimentieren Sie. Denken Sie: map für normale Werte, chain für Funktoren-Rückgaben. Nächstes Kapitel zeigt Applikative für besser lesbare Ausdrücke.
Hinweis: Dies funktioniert nicht mit verschiedenen verschachtelten Typen. Hier helfen Funktor-Komposition und Monad-Transformer.
Machtrausch
Container-Programmierung kann verwirren: Wie tief sind Werte verschachtelt? Wann map, wann chain? Debugging-Hilfen wie inspect und typisierte Stacks mildern dies.
Lassen Sie mich monadische Stärke demonstrieren:
Lesen und sofort hochladen einer Datei:
// readFile :: Filename -> Either String (Task Error String)
// httpPost :: String -> String -> Task Error JSON
// upload :: Filename -> Either String (Task Error JSON)
const upload = compose(map(chain(httpPost('/uploads'))), readFile);Hier verzweigt der Code und behandelt drei Fehlerquellen: Either prüft Dateinamen, Task Dateizugriffsfehler, httpPost Upload-Fehler. Mit chain bewältigen wir zwei verschachtelte asynchrone Aktionen mühelos.
Alles rein funktional und deklarativ in einem Fluss. Gleichungsbasiertes Denken bleibt erhalten. Die upload-Funktion arbeitet gegen generische Schnittstellen - in nur einer Zeile!
Vergleichsweise die imperative Variante:
// upload :: Filename -> (String -> a) -> Void
const upload = (filename, callback) => {
if (!filename) {
throw new Error('You need a filename!');
} else {
readFile(filename, (errF, contents) => {
if (errF) throw errF;
httpPost('/uploads', contents, (errH, json) => {
if (errH) throw errH;
callback(json);
});
});
}
};Ein Albtraum! Wir springen durch einen Fehlerparcours. Würden Variablen mutiert, wäre Chaos perfekt.
Theorie
Das erste Gesetz betrifft Assoziativität - allerdings anders als gewohnt.
// associativity
compose(join, map(join)) === compose(join, join);Diese Gesetze behandeln Monaden-Verschachtelung. Assoziativität bedeutet: Die Reihenfolge des join-Aufrufs ist irrelevant. Veranschaulichung:

Bei M(M(M a)) können wir entweder die äußeren oder inneren M-Schichten zuerst mit join entfernen. Beide Wege führen zum gleichen M a. Zwischenschritte variieren, das Endergebnis bleibt gleich.
Das zweite Gesetz:
// identity for all (M a)
compose(join, of) === compose(join, map(of)) === id;Für jede Monade M gilt: of mit join entspricht id. Visualisierung als Dreieck:

Richtungsunabhängig sehen wir: Das Einsetzen in und Ausführen der Monade ändert nichts am Ursprungswert.
Wichtig: of muss monadenspezifisch sein (z.B. M.of).
Diese Gesetze erinnern an Kategorienaxiome. Richtig - Monaden bilden eine „Kleisli-Kategorie“, wo Objekte Monaden und Morphismen verkettete Funktionen sind.
const mcompose = (f, g) => compose(chain(f), g);
// left identity
mcompose(M, f) === f;
// right identity
mcompose(f, M) === f;
// associativity
mcompose(mcompose(f, g), h) === mcompose(f, mcompose(g, h));Ich will nicht mit Kategorientheorie verwirren, sondern praktische Relevanz zeigen.
Zusammenfassung
Monaden ermöglichen den Zugriff auf verschachtelte Berechnungen. Sie verwalten Variablen, Effekte und Asynchronität ohne Callback-Hölle. Mit „pointed“ als Partner helfen sie beim entpackten Arbeiten.
Trotz ihrer Stärke brauchen wir weitere Container-Funktionen: Parallele API-Aufrufe? Validierungssammlungen? Hier sind Applikative Funktoren besser geeignet.
Nächstes Kapitel: Applikative Funktoren und ihre Vorteile gegenüber Monaden.
Kapitel 10: Applikative Funktoren
Übungen
Gegeben ein User-Objekt:
const user = {
id: 1,
name: 'Albert',
address: {
street: {
number: 22,
name: 'Walnut St',
},
},
};Let's Practice!
Verwenden Sie safeProp und map/join oder chain, um den Straßennamen eines Benutzers sicher auszulesen
// getStreetName :: User -> Maybe String
const getStreetName = undefined;
Betrachten Sie folgende Elemente:
// getFile :: IO String
const getFile = IO.of('/home/mostly-adequate/ch09.md');
// pureLog :: String -> IO ()
const pureLog = str => new IO(() => console.log(str));Let's Practice!
Nutzen Sie getFile, um den Dateipfad zu holen, entfernen Sie das Verzeichnis und loggen Sie den reinen Dateinamen. Tipp: Verwenden Sie split und last zur Namensextraktion.
// logFilename :: IO ()
const logFilename = undefined;
Hilfsfunktionen mit folgenden Signaturen:
// validateEmail :: Email -> Either String Email
// addToMailingList :: Email -> IO([Email])
// emailBlast :: [Email] -> IO ()Let's Practice!
Bauen Sie mit validateEmail, addToMailingList und emailBlast eine Funktion, die valide E-Mails in die Mailingliste aufnimmt und alle benachrichtigt.
// joinMailingList :: Email -> Either String (IO ())
const joinMailingList = undefined;