Kapitel 11: Erneut transformieren, natürlich
Wir werden nun natürliche Transformationen im Kontext ihrer praktischen Anwendung im Alltagscode diskutieren. Sie sind nicht nur ein Grundpfeiler der Kategorientheorie, sondern absolut unverzichtbar, wenn wir Mathematik nutzen, um unseren Code zu analysieren und zu refaktorieren. Daher halte ich es für meine Pflicht, Sie über die bedauerliche Ungenauigkeit zu informieren, der Sie zweifellos aufgrund meines begrenzten Umfangs begegnen werden. Fangen wir an.
Fluch dieser Verschachtelung
Ich möchte das Problem der Verschachtelung ansprechen – nicht den instinktiven Ordnungsdrang werdender Eltern, sondern... nun, bei genauerem Nachdenken ist die Analogie gewissermaßen wie eine Neugeborenenwiege gar nicht so abwegig, wie wir in den kommenden Kapiteln sehen werden... Gemeint sind hier Typen, die sich schützend um einen Wert schichten.
Right(Maybe('b'));
IO(Task(IO(1000)));
[Identity('bee thousand')];Bisher haben wir dieses Szenario mit sorgfältig konstruierten Beispielen vermieden. Doch in der Praxis verknoten sich Typen wie Kopfhörerkabel bei der Exorzismus. Wenn wir sie nicht systematisch organisieren, wird unser Code unübersichtlicher als ein Beatnik im Katzenkaffee.
Eine sitcom der Typen
// getValue :: Selector -> Task Error (Maybe String)
// postComment :: String -> Task Error Comment
// validate :: String -> Either ValidationError String
// saveComment :: () -> Task Error (Maybe (Either ValidationError (Task Error Comment)))
const saveComment = compose(
map(map(map(postComment))),
map(map(validate)),
getValue('#comment'),
);Die ganze Bande ist versammelt – zur Verzweiflung unserer Typsignaturen. Kurz erklärt: getValue('#comment') holt Benutzereingaben und gibt Task Error (Maybe String) zurück, da DOM-Abfragen fehlschlagen können. Mit map bearbeiten wir sowohl Task als auch Maybe, um den Text an validate zu übergeben, was ein Either ValidationError String liefert. Nach weiteren map-Operationen landen wir bei Task Error (Maybe (Either ValidationError String)), das an postComment übergeben wird.
Ein grauenhaftes Durcheinander. Ein Abstract-Type-Collage, typografischer Expressionismus, polymorpher Pollock. Lösungsansätze gibt es viele: monströse Container, Typvereinheitlichung durch natürliche Transformationen – darauf konzentrieren wir uns hier.
Ganz natürlich
Eine natürliche Transformation ist ein „Morphismus zwischen Funktoren“ – eine Funktion, die Container strukturell transformiert. Typisiert als (Functor f, Functor g) => f a -> g a. Entscheidend: Wir dürfen den Containerinhalt nicht einsehen. Formal muss gelten:

Oder in Code:
// nt :: (Functor f, Functor g) => f a -> g a
compose(map(f), nt) === compose(nt, map(f));Diagramm und Code besagen: Die Reihenfolge von Transformation und map ist beliebig. Dies folgt aus einem freien Theorem, wobei natürliche Transformationen nicht auf Typfunktionen beschränkt sind.
Prinzipientreue Typkonversionen
Als Entwickler sind Typkonversionen vertraut (z.B. String → Boolean). Hier geht es um algebraische Container mit theoretischem Unterbau.
Beispiele:
// idToMaybe :: Identity a -> Maybe a
const idToMaybe = x => Maybe.of(x.$value);
// idToIO :: Identity a -> IO a
const idToIO = x => IO.of(x.$value);
// eitherToTask :: Either a b -> Task a b
const eitherToTask = either(Task.rejected, Task.of);
// ioToTask :: IO a -> Task () a
const ioToTask = x => new Task((reject, resolve) => resolve(x.unsafePerform()));
// maybeToTask :: Maybe a -> Task () a
const maybeToTask = x => (x.isNothing ? Task.rejected() : Task.of(x.$value));
// arrayToMaybe :: [a] -> Maybe a
const arrayToMaybe = x => Maybe.of(x[0]);Das Prinzip? Ein Funktor wird in einen anderen umgewandelt. Informationsverlust ist solange erlaubt, wie der map-Flow erhalten bleibt. Das ist entscheidend: map muss nach der Transformation weiterhin funktionieren.
Man kann dies als Effekttransformation sehen: ioToTask wandelt synchron → asynchron, arrayToMaybe Nichtdeterminismus → Fehlerbehandlung. Die Umkehrung taskToIO wäre in JavaScript eine „übernatürliche“ Transformation.
Funktionsneid
Möchte man Methoden wie sortBy von List nutzen, bieten natürliche Transformationen eine elegante Konvertierungslösung bei typsicherer map-Komposition.
// arrayToList :: [a] -> List a
const arrayToList = List.of;
const doListyThings = compose(sortBy(h), filter(g), arrayToList, map(f));
const doListyThings_ = compose(sortBy(h), filter(g), map(f), arrayToList); // law appliedMit arrayToList verwandeln wir [a] in List a – schon lässt sich sortBy anwenden.
Durch Vorziehen von map(f) vor die Transformation (wie in doListyThings_) wird Operationsoptimierung vereinfacht.
Isomorphes JavaScript
Bei bidirektional verlustfreier Konvertierung liegt ein Isomorphismus („gleiche Datenhaltung“) vor. Zwei Typen sind isomorph, wenn sie durch „to“/„from“-Transformationen verbunden sind:
// promiseToTask :: Promise a b -> Task a b
const promiseToTask = x => new Task((reject, resolve) => x.then(resolve).catch(reject));
// taskToPromise :: Task a b -> Promise a b
const taskToPromise = x => new Promise((resolve, reject) => x.fork(reject, resolve));
const x = Promise.resolve('ring');
taskToPromise(promiseToTask(x)) === x;
const y = Task.of('rabbit');
promiseToTask(taskToPromise(y)) === y;Damit sind Promise und Task isomorph. Mit listToArray + arrayToList gilt dies auch für Listen/Arrays. arrayToMaybe hingegen ist kein Isomorphismus, da Information verlorengeht:
// maybeToArray :: Maybe a -> [a]
const maybeToArray = x => (x.isNothing ? [] : [x.$value]);
// arrayToMaybe :: [a] -> Maybe a
const arrayToMaybe = x => Maybe.of(x[0]);
const x = ['elvis costello', 'the attractions'];
// not isomorphic
maybeToArray(arrayToMaybe(x)); // ['elvis costello']
// but is a natural transformation
compose(arrayToMaybe, map(replace('elvis', 'lou')))(x); // Just('lou costello')
// ==
compose(map(replace('elvis', 'lou')), arrayToMaybe)(x); // Just('lou costello')Es handelt sich zwar um natürliche Transformationen, da map auf beiden Seiten zum gleichen Ergebnis führt, aber dennoch erwähne ich hier mittendrin Isomorphismen – lassen Sie sich nicht täuschen, sie sind ein äußerst mächtiges und allgegenwärtiges Konzept.
Erweiterte Definition
Diese strukturellen Funktionen beschränken sich nicht auf Typkonversionen:
Weitere Beispiele:
reverse :: [a] -> [a]
join :: (Monad m) => m (m a) -> m a
head :: [a] -> a
of :: a -> f aDie Naturtransformationsgesetze gelten auch hier. Man beachte: head :: [a] -> a kann als head :: [a] -> Identity a betrachtet werden. Wir können Identity nach Belieben in Beweisen einsetzen, da a isomorph zu Identity a ist – wie gesagt, Isomorphismen sind allgegenwärtig.
Lösung für Verschachtelung
Zurück zu unserer Typsignatur-Komödie: Durch strategisches Einfügen von natürlichen Transformationen vereinheitlichen wir die Typen für join-Operationen.
// getValue :: Selector -> Task Error (Maybe String)
// postComment :: String -> Task Error Comment
// validate :: String -> Either ValidationError String
// saveComment :: () -> Task Error Comment
const saveComment = compose(
chain(postComment),
chain(eitherToTask),
map(validate),
chain(maybeToTask),
getValue('#comment'),
);Durch chain(maybeToTask) und chain(eitherToTask) transformieren wir die inneren Funktoren zu Task und vereinen sie – wie Taubenspikes an Simsen verhindern wir Verschachtelung an der Quelle. Oder wie man in Paris sagt: „Mieux vaut prévenir que guérir“ – Vorbeugen ist besser als heilen.
Zusammenfassung
Natürliche Transformationen als Funktoroperationen sind kategorientheoretisch essenziell. Sie ermöglichen Effekttransformationen und Typvereinheitlichung, oft zugunsten des „dominantesten“ Funktors (meist Task).
Dieser typorganisatorische Aufwand ist der Preis materialisierter Effekte. Mit weiteren Werkzeugen wie Traversable (nächstes Kapitel) meistern wir größere Typamalgame.
Kapitel 12: Traversable – Der Stein der Weisen
Übungen
Let's Practice!
Schreiben Sie eine natürliche Transformation von Either b a zu Maybe a
// eitherToMaybe :: Either b a -> Maybe a
const eitherToMaybe = undefined;
// eitherToTask :: Either a b -> Task a b
const eitherToTask = either(Task.rejected, Task.of);Let's Practice!
Vereinfachen Sie findNameById mit eitherToTask, um die Either-Verschachtelung zu entfernen.
// findNameById :: Number -> Task Error (Either Error User)
const findNameById = compose(map(map(prop('name'))), findUserById);
Hinweis: Folgende Funktionen stehen im Übungskontext zur Verfügung:
split :: String -> String -> [String]
intercalate :: String -> [String] -> StringLet's Practice!
Schreiben Sie Isomorphismen zwischen String und [Char]. // strToList :: String -> [Char] const strToList = undefined; // listToStr :: [Char] -> String const listToStr = undefined;