Skip to content

Kapitel 06: Beispielanwendung

Deklarative Programmierung

Wir müssen unsere Denkweise ändern. Ab jetzt werden wir nicht mehr dem Computer vorschreiben, wie er seine Arbeit erledigen soll, sondern Spezifikationen für die gewünschten Ergebnisse schreiben. Sie werden feststellen, dass dieser Ansatz wesentlich entspannter ist als die mikromanagement-basierte Steuerung jedes Details.

Deklarative Programmierung (im Gegensatz zu imperativ) bedeutet, dass wir Ausdrücke statt Schritt-für-Schritt-Anweisungen formulieren.

Betrachten Sie SQL: Es gibt kein »Erst mache dies, dann mache das«. Ein einzelner Ausdruck spezifiziert, was wir von der Datenbank möchten. Die Implementierungsdetails überlassen wir dem System. Bei Datenbank-Updates oder SQL-Engine-Optimierungen müssen unsere Abfragen nicht geändert werden, da es viele Wege gibt, unsere Spezifikation zu interpretieren und zum selben Ergebnis zu kommen.

Für viele – mich eingeschlossen – ist das Konzept deklarativer Programmierung zunächst schwer zu erfassen. Lassen Sie uns daher einige Beispiele betrachten.

js
// imperative
const makes = [];
for (let i = 0; i < cars.length; i += 1) {
  makes.push(cars[i].make);
}

// declarative
const makes = cars.map(car => car.make);

Der imperative Loop muss zuerst das Array instanziieren. Der Interpreter wertet diese Anweisung aus, bevor er fortfährt. Anschließend durchläuft er explizit die Autoliste, erhöht manuell einen Zähler und präsentiert uns in plump sichtbarer Iteration einzelne Fragmente.

Die map-Variante ist ein einzelner Ausdruck. Sie erfordert keine bestimmte Auswertungsreihenfolge. Die Umsetzung der Iteration und die Zusammenstellung des resultierenden Arrays bleibt dem System überlassen. Sie spezifiziert das Was, nicht das Wie – daher verdient sie das Prädikat ›deklarativ‹.

Neben größerer Klarheit und Kürze kann die map-Funktion nach Belieben optimiert werden, ohne dass dies Änderungen am Anwendungscode erfordert.

Für alle, die denken »Aber imperativ ist doch viel schneller«: Studieren Sie die JIT-Optimierungstechniken. Ein aufschlussreiches Video dazu

Ein weiteres Beispiel:

js
// imperative
const authenticate = (form) => {
  const user = toUser(form);
  return logIn(user);
};

// declarative
const authenticate = compose(logIn, toUser);

Obwohl die imperative Version nicht falsch ist, kodiert sie dennoch eine feste Ausführungsreihenfolge. Der compose-Ausdruck stellt einfach eine Tatsache fest: Authentifizierung ist die Komposition von toUser und logIn. Dies lässt Spielraum für Code-Anpassungen und erhält den Spezifikations-Charakter der Applikation.

Im obigen Beispiel ist die Auswertungsreihenfolge festgelegt (toUser muss vor logIn aufgerufen werden). In vielen Fällen ist die Reihenfolge jedoch irrelevant – genau dies lässt sich mit deklarativer Programmierung einfach spezifizieren (mehr dazu später).

Durch den Verzicht auf Ausführungsreihenfolge eignet sich deklarative Programmierung besonders für parallele Berechnungen. Kombiniert mit reinen Funktionen macht dies FP zur idealen Wahl für parallele Systeme – ohne zusätzlichen Aufwand.

Funktionale Programmierung in Aktion: Eine Flickr-Anwendung

Wir entwickeln nun eine deklarative, komponierbare Beispielanwendung. Nebenwirkungen werden vorerst noch begrenzt eingesetzt und vom puren Code separiert. Ziel ist ein Widget, das Flickr-Bilder lädt und anzeigt. Grundgerüst in HTML:

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Flickr App</title>
  </head>
  <body>
    <main id="js-main" class="main"></main>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.2.0/require.min.js"></script>
    <script src="main.js"></script>
  </body>
</html>

Skelett der main.js-Datei:

js
const CDN = s => `https://cdnjs.cloudflare.com/ajax/libs/${s}`;
const ramda = CDN('ramda/0.21.0/ramda.min');
const jquery = CDN('jquery/3.0.0-rc1/jquery.min');

requirejs.config({ paths: { ramda, jquery } });
requirejs(['jquery', 'ramda'], ($, { compose, curry, map, prop }) => {
  // app goes here
});

Wir verwenden Ramda (statt lodash) mit seinen Funktionen compose und curry. RequireJS dient der einheitlichen Modularisierung.

Unsere App muss vier Aufgaben erfüllen:

  1. URL-Erstellung für Suchbegriffe
  2. Flickr-API-Aufruf
  3. Transformation des JSON-Ergebnisses in HTML-Bilder
  4. Darstellung auf dem Bildschirm

Zwei dieser Aktionen sind impur: API-Aufruf und DOM-Manipulation. Diese separieren wir zunächst. Hilfsfunktion trace zum Debugging:

js
const Impure = {
  getJSON: curry((callback, url) => $.getJSON(url, callback)),
  setHtml: curry((sel, html) => $(sel).html(html)),
  trace: curry((tag, x) => { console.log(tag, x); return x; }),
};

Wir kapseln jQuery-Methoden in curried Funktionen mit angepasster Parameterreihenfolge. Der ›Impure‹-Namespace kennzeichnet diese unsicheren Funktionen.

Konstruktion der API-URL:

js
const host = 'api.flickr.com';
const path = '/services/feeds/photos_public.gne';
const query = t => `?tags=${t}&format=json&jsoncallback=?`;
const url = t => `https://${host}${path}${query(t)}`;

Statt komplexer monoidaler Implementierungen wählen wir lesbare String-Konkatenierung.

Hauptfunktion für API-Aufruf und Darstellung:

js
const app = compose(Impure.getJSON(Impure.trace('response')), url);
app('cats');

Die Funktion kombiniert URL-Generierung mit dem curried getJSON-Aufruf. Die Antwort erscheint in der Konsole.

console response

Die Medien-URLs liegen in items[i].media.m. Zum Extrahieren verwenden wir Ramdas prop:

Einfache Property-Zugriffsfunktion (Demonstration):

js
const prop = curry((property, object) => object[property]);

Praktische Anwendung mit []-Syntax:

js
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));

Nachdem wir items extrahiert haben, mappen wir jedes Element zur Medien-URL. Integration in die App:

js
const render = compose(Impure.setHtml('#js-main'), mediaUrls);
const app = compose(Impure.getJSON(render), url);

Die Komposition ruft mediaUrls auf und füllt das <main>-Element. Statt trace verwenden wir nun render.

Umwandlung der URLs in <img>-Tags (statt Templates):

js
const img = src => $('<img />', { src });

jQuerys html-Methode verarbeitet Tag-Arrays. Transformation der URLs in Bildtags:

js
const images = compose(map(img), mediaUrls);
const render = compose(Impure.setHtml('#js-main'), images);
const app = compose(Impure.getJSON(render), url);

Fertig!

cats grid

Vollständiger Code: include

Das Ergebnis ist eine elegante deklarative Spezifikation des »Was«. Jede Codezeile entspricht einer verifizierbaren Gleichung – Basis für Refactoring und logisches Reasoning.

Prinzipiengeleitetes Refactoring

Optimierungsmöglichkeit durch Map-Kompositionsgesetz:

js
// map's composition law
compose(map(f), map(g)) === map(compose(f, g));

Anwendung des Gesetzes zur Codeverbesserung:

js
// original code
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));
const images = compose(map(img), mediaUrls);

Vereinfachung durch Inline-Aufrufe und Gleichungsbasierte Transformation:

js
const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(img), map(mediaUrl), prop('items'));

Zusammenführung der map-Aufrufe:

js
/*
compose(map(f), map(g)) === map(compose(f, g));
compose(map(img), map(mediaUrl)) === map(compose(img, mediaUrl));
*/

const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(compose(img, mediaUrl)), prop('items'));

Einzelner Durchlauf für effiziente Tag-Generierung, lesbar durch extrahierte Funktion:

js
const mediaUrl = compose(prop('m'), prop('media'));
const mediaToImg = compose(img, mediaUrl);
const images = compose(map(mediaToImg), prop('items'));

Zusammenfassung

Wir haben gezeigt, wie funktionale Konzepte in realen Apps angewendet werden. Das mathematische Framework ermöglicht logisches Refactoring. Offene Fragen: Fehlerbehandlung, vollständig pure Anwendungen, Sicherheit – dies behandeln wir in Teil 2.

Kapitel 07: Hindley-Milner und Ich