Kapitel 10: Applikative Funktoren
Anwendung von Applikativen
Der Name applikativer Funktor ist angesichts seiner funktionalen Ursprünge erfreulich deskriptiv. Funktionalprogrammierer sind berüchtigt für Namen wie mappend oder liftA4, die im mathematischen Kontext natürlich wirken, aber in jedem anderen Rahmen so klar sind wie ein unentschlossener Darth Vader am Drive-in.
Jedenfalls verrät der Name bereits die Kernfunktion dieser Schnittstelle: Die Fähigkeit, Funktoren aufeinander anzuwenden.
Warum sollte eine normale, rational denkende Person so etwas wollen? Was bedeutet es überhaupt, einen Funktor auf einen anderen anzuwenden?
Um diese Fragen zu beantworten, beginnen wir mit einer Situation aus der funktionalen Praxis. Es werden zwei Funktoren (desselben Typs) betrachtet, auf deren Werte eine Funktion angewendet werden soll. Beispielsweise das Addieren der Werte zweier Container.
// We can't do this because the numbers are bottled up.
add(Container.of(2), Container.of(3));
// NaN
// Let's use our trusty map
const containerOfAdd2 = map(add, Container.of(2));
// Container(add(2))Hier liegt ein Container mit partiell angewendeter Funktion vor. Konkret: Ein Container(add(2)) soll sein add(2) auf die 3 in Container(3) anwenden. Mit anderen Worten: Ein Funktor soll auf einen anderen angewendet werden.
Es zeigen sich bereits vorhandene Werkzeuge zur Lösung. Das partielle add(2) kann mittels chain und map angewendet werden:
Container.of(2).chain(two => Container.of(3).map(add(two)));Problem hierbei ist die sequentielle Natur von Monaden, wo die Auswertung erst nach Abschluss der vorherigen Monade erfolgt. Bei unabhängigen Werten wie diesen ist die Verzögerung von Container(3)-Erstellung nur für monadische Anforderungen unnötig.
Idealerweise könnten Funktorinhalte präzise auf andere Werte angewendet werden - ohne redundante Funktionen und Variablen.
Schiffe in Flaschen

ap ist eine Funktion zur Anwendung von Funktionsinhalten eines Funktors auf Wertinhalte eines anderen. Fünfmal schnell hinterholt.
Container.of(add(2)).ap(Container.of(3));
// Container(5)
// all together now
Container.of(2).map(add).ap(Container.of(3));
// Container(5)Damit ist Container(3) aus der Verschachtelung monadischer Funktionen befreit. Wichtig: add wird hier beim ersten map partiell angewendet, was nur mit curry-gebratenen Funktionen funktioniert.
ap kann folgendermaßen definiert werden:
Container.prototype.ap = function (otherContainer) {
return otherContainer.map(this.$value);
};Da this.$value eine Funktion ist und ein weiterer Funktor vorliegt, genügt einfaches map. Daraus ergibt sich die Schnittstellendefinition:
Ein applikativer Funktor ist ein pointed Funktor mit einer
ap-Methode
Beachten Sie die pointed-Abhängigkeit. Diese Schnittstelle ist entscheidend, wie folgende Beispiele zeigen.
Zweifel sind natürlich, doch ap erweist sich als nützlich. Zuvor betrachten wir eine elegante Eigenschaft.
F.of(x).map(f) === F.of(f).ap(F.of(x));Formal: f mappen entspricht ap auf einem Funktor von f. Praktisch: x wird in den Container platziert und map(f) angewendet, oder f und x werden gemeinsam angehoben und ap genutzt. Dies ermöglicht links-nach-rechts-Schreibweise:
Maybe.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
// Maybe(5)
Task.of(add).ap(Task.of(2)).ap(Task.of(3));
// Task(5)Bei halbgeschlossenen Augen lassen sich normale Funktionsaufrufe erahnen. Durch of gelangen Werte in die Container-Welt, wo ap Funktionen asynchron oder null-sicher verarbeitet - wie ein Schiff in der Flasche.
Das Task-Bezeigt hier die Stärke applikativer Funktoren. Ein detaillierteres Beispiel:
Koordinationsmotivation
Betrachten wir eine Reiseplattform mit getrennten API-Abfragen für Touristenziele und Lokalereignisse.
// Http.get :: String -> Task Error HTML
const renderPage = curry((destinations, events) => { /* render page */ });
Task.of(renderPage).ap(Http.get('/destinations')).ap(Http.get('/events'));
// Task("<div>some page with dest and events</div>")Beide Http-Aufrufe erfolgen parallel, renderPage wird nach beiden Resolutionen aufgerufen. Im Gegensatz zu monadischer sequentieller Abarbeitung. Da Events nicht von Zielen abhängen, entfällt diese Einschränkung.
Da partielle Anwendung genutzt wird, muss renderPage curry-gebraten sein. Manuelle Implementierer werden die Einfachheit dieser Schnittstelle zu schätzen wissen - ein Schritt Richtung technologischer Singularität.
Weiteres Beispiel:
// $ :: String -> IO DOM
const $ = selector => new IO(() => document.querySelector(selector));
// getVal :: String -> IO String
const getVal = compose(map(prop('value')), $);
// signIn :: String -> String -> Bool -> User
const signIn = curry((username, password, rememberMe) => { /* signing in */ });
IO.of(signIn).ap(getVal('#email')).ap(getVal('#password')).ap(IO.of(false));
// IO({ id: 3, email: 'gg@allin.com' })signIn ist eine curry-gebraten Funktion mit 3 Argumenten. Jedes ap fügt ein Argument hinzu. Beachten Sie: Die ersten zwei Argumente sind natürlich in IO, das letzte benötigt of für Typkonsistenz.
Bruder, liftest du überhaupt?
Betrachtung pointfree-Schreibweise. Da map gleich of/ap ist, lassen sich generische Funktionen erstellen:
const liftA2 = curry((g, f1, f2) => f1.map(g).ap(f2));
const liftA3 = curry((g, f1, f2, f3) => f1.map(g).ap(f2).ap(f3));
// liftA4, etcliftA2 klingt wie ein kapriziöser Lastenaufzug. Doch die Bedeutung ist klar: Elemente in die Applikative-Welt heben.
Die 2-3-4-Nummer erschien anfangs unattraktiv. Dabei lässt sich die Stelligkeit in JavaScript dynamisch ermitteln. Für partielle Anwendung von liftA(N) selbst sind feste Argumentlängen jedoch nützlich.
Anwendungsbeispiel:
// checkEmail :: User -> Either String Email
// checkName :: User -> Either String String
const user = {
name: 'John Doe',
email: 'blurp_blurp',
};
// createUser :: Email -> String -> IO User
const createUser = curry((email, name) => { /* creating... */ });
Either.of(createUser).ap(checkEmail(user)).ap(checkName(user));
// Left('invalid email')
liftA2(createUser, checkEmail(user), checkName(user));
// Left('invalid email')Da createUser zwei Argumente erwartet, kommt liftA2 zum Einsatz. Die liftA2-Version ist generischer und nicht an Either gebunden.
Vorherige Beispiele in dieser Schreibweise:
liftA2(add, Maybe.of(2), Maybe.of(3));
// Maybe(5)
liftA2(renderPage, Http.get('/destinations'), Http.get('/events'));
// Task('<div>some page with dest and events</div>')
liftA3(signIn, getVal('#email'), getVal('#password'), IO.of(false));
// IO({ id: 3, email: 'gg@allin.com' })Operatoren
In Sprachen mit benutzerdefinierten Infix-Operatoren (Haskell etc.) findet man:
-- Haskell / PureScript
add <$> Right 2 <*> Right 3// JavaScript
map(add, Right(2)).ap(Right(3));Hinweis: <$> ist map (alias fmap), <*> ist ap. Dies ermöglicht natürlichere Funktionsanwendung mit weniger Klammern.
Kostenlose Dosenöffner

Abgeleitete Funktionen: Da Schnittstellen aufeinander aufbauen, lassen sich schwächere aus stärkeren definieren.
Jeder Applikative ist ein Funktor, daher lässt sich bei vorhandener Instanz ein Funktor ableiten.
Diese mathematische Harmonie übertrifft selbst Mozarts Kompositionskunst.
Wie erwähnt entspricht of/ap dem map. Damit lässt sich map definieren:
// map derived from of/ap
X.prototype.map = function map(f) {
return this.constructor.of(f).ap(this);
};Monaden stehen hierarchisch oben. Mit chain erhalten wir Funktor und Applikative kostenlos:
// map derived from chain
X.prototype.map = function map(f) {
return this.chain(a => this.constructor.of(f(a)));
};
// ap derived from chain/map
X.prototype.ap = function ap(other) {
return this.chain(f => other.map(f));
};Monaden-Implementierung liefert automatisch Applikative und Funktoren. Bemerkenswerte kostenfreie Funktionalität, sogar automatisierbar.
aps Parallelitätsvorteil geht bei chain-Definition verloren. Dennoch bietet ein funktionierendes Interface gute Basisoptimierung.
Warum nicht nur Monaden nutzen? Minimal notwendige Abstraktion reduziert kognitive Last. Applikatives sind bei Verzicht auf monadische Features vorzuziehen.
Monaden ermöglichen Sequenzierung, Variablenbindung und Abbruch. Applikatives entbinden davon.
Nun zu den Gesetzmäßigkeiten...
Gesetze
Applikative Funktoren folgen nützlichen Gesetzen: Unter Komposition abgeschlossen (ap ändert Containertypen nicht). Effektstapelung bei Typkonsistenz möglich.
Demonstration:
const tOfM = compose(Task.of, Maybe.of);
liftA2(liftA2(concat), tOfM('Rainy Days and Mondays'), tOfM(' always get me down'));
// Task(Maybe(Rainy Days and Mondays always get me down))Typenkonsistenz bleibt gewahrt.
Zentrales Gesetz: Identität:
Identität
// identity
A.of(id).ap(v) === v;id innerhalb eines Funktors darf v nicht verändern. Beispiel:
const v = Identity.of('Pillow Pets');
Identity.of(id).ap(v) === v;Identity.of(id) erscheint trivial. Da of/ap gleich map ist, folgt dies aus map(id) == id.
Diese Gesetze erzwingen Schnittstellenkonsistenz - wie ein strenger Sportlehrer.
Homomorphismus
// homomorphism
A.of(f).ap(A.of(x)) === A.of(f(x));Ein Homomorphismus bewahrt Strukturen. Funktoren sind Kategorie-Homomorphismen durch Strukturerhaltung.
Berechnungen in Containern liefern gleiche Ergebnisse wie externe Anwendung mit nachträglichem Einpacken.
Beispiel:
Either.of(toUpperCase).ap(Either.of('oreos')) === Either.of(toUpperCase('oreos'));Austauschbarkeit
Das Austauschbarkeitsgesetz: Die Seite des ap-Aufrufs (links/rechts) ist irrelevant.
// interchange
v.ap(A.of(x)) === A.of(f => f(x)).ap(v);Beispiel:
const v = Task.of(reverse);
const x = 'Sparklehorse';
v.ap(Task.of(x)) === Task.of(f => f(x)).ap(v);Komposition
Überprüfung der Standardfunktionskomposition innerhalb von Containern.
// composition
A.of(compose).ap(u).ap(v).ap(w) === u.ap(v.ap(w));const u = IO.of(toUpperCase);
const v = IO.of(concat('& beyond'));
const w = IO.of('blood bath ');
IO.of(compose).ap(u).ap(v).ap(w) === u.ap(v.ap(w));Zusammenfassung
Applikatives eignen sich bei multiplen Funktor-Argumenten. Ermöglichen Funktionsanwendung in der Funktor-Welt. Bevorzugung gegenüber Monaden bei entbehrlichen Features.
Container-APIs fast abgeschlossen. map, chain, ap beherrscht. Nächstes Kapitel behandelt effizienten Umgang mit multiplen Funktoren.
Kapitel 11: Nochmal Transformation, natürlich
Übungen
Let's Practice!
Schreiben Sie eine Funktion, die zwei möglicherweise null-Werte mit Maybe und ap addiert.
// safeAdd :: Maybe Number -> Maybe Number -> Maybe Number
const safeAdd = undefined;
Let's Practice!
Schreiben Sie safeAdd aus Übung_b mit liftA2 statt ap um.
// safeAdd :: Maybe Number -> Maybe Number -> Maybe Number
const safeAdd = undefined;
Für die nächste Übung verwenden wir folgende Hilfsmittel:
const localStorage = {
player1: { id:1, name: 'Albert' },
player2: { id:2, name: 'Theresa' },
};
// getFromCache :: String -> IO User
const getFromCache = x => new IO(() => localStorage[x]);
// game :: User -> User -> String
const game = curry((p1, p2) => `${p1.name} vs ${p2.name}`);Let's Practice!
Erstellen Sie einen IO, der player1 und player2 aus dem Cache holt und das Spiel startet. // startGame :: IO String const startGame = undefined;