Webkrauts Logo

Webkrauts Webkrauts Schriftzug

- für mehr Qualität im Web

Die absoluten Grundlagen funktionaler Programmierung für absolute Anfänger

JavaScript

Die absoluten Grundlagen funktionaler Programmierung für absolute Anfänger

In der imperativen Programmierung besteht ein Programm aus einer Folge von Anweisungen, die in der Regel Variablen verändern – das ist die bekannte Methode. Bei der funktionalen Programmierung bestehen Programme ausschließlich aus Funktionen. Einsteiger werden oft von vielen seltsamen Fachbegriffe abgeschreckt, dabei sind die Grundlagen funktionaler Programmierung eigentlich einfach zu verstehen.

Dieser Tage ist funktionale Programmierung (und speziell funktionales JavaScript) in aller Munde, wird dabei aber übermäßig verkompliziert. Viele komische Fachbegriffe schrecken den eigentlich geneigten Einsteiger ab, und überhaupt ist das ja alles auch nicht praktisch einsetzbar, sondern nur etwas für selbsterklärte Alpha-Nerds, die sich mit ihrem Haskell-Nebenprojekt auf Twitter profilieren möchten. Oder? Nicht ganz. Eigentlich sind die Grundlagen total einfach zu verstehen und mit Functional Reactive Programming gibt es auch eine alltagskompatible Anwendung für funktionale Programmierung. Dieser Artikel möchte die absoluten Grundlagen funktionaler Programmierung für absolute Anfänger (in Sachen funktionaler Programmierung) erklären. Inspiriert wurde dieser Artikel von einem Talk zum gleichen Thema, den Anouk Ruhaak auf der OpenTechSchool Conference in Dortmund präsentiert hat.

Grundsätzlich kann man funktionales Vorgehen auf wenige Kern-Prinzipien zusammendampfen. Manche Programmiersprachen setzen diese Prinzipien direkt in der Sprache um, bei anderen (u.a. JavaScript) muss sich der Programmierer selbst um deren Einhaltung kümmern. Die beiden wichtigsten Prinzipien für funktionale Programmierung sind:

  1. Funktionen haben Inputs und Outputs, aber keine Nebenwirkungen. Sie bearbeiten keine Daten, die ihnen nicht explizit übergeben wurden (sie sind so genannte pure functions).
  2. Daten („Variablen“) werden nie wirklich verändert, sondern dienen nur als statischer Ausgangspunkt für die Erzeugung anderer statischer Daten (immutability).

Ein einfaches Beispiel

Zu einem gewissen Grad gehen beide genannten Punkte Hand in Hand. Durch die Einhaltung dieser beiden Prinzipien können wir einiges gewinnen und sofern eine Programmiersprache ein paar bestimmte Features aufweist, verliert sie durch die Einhaltung der Prinzipien kein Stück an Mächtigkeit. Nur die für den Code verantwortliche Person muss unter Umständen ein wenig umdenken. Werfen wir zunächst mal einen Blick auf einen Programmschnipsel, der die o.g. Prinzipien nicht umsetzt:

  1. var door = {
  2.   open: false
  3. };
  4.  
  5. function toggleDoor(){
  6.   door.open = !door.open;
  7. }
  8.  
  9. toggleDoor();

Dieses Beispielprogramm ist naturgemäß sehr simpel und macht genau das, was es tun soll. Aber trotzdem lassen sich einige Kritikpunkte finden:

  1. toggleDoor() ist mit dem Rest des Programms (dem globalen Objekt door) fest verdrahtet und daher nicht isoliert und portabel. Wir können die Funktion nicht einfach in ein anderes Projekt übernehmen und dort andere Dinge wie z.B. Brotdosen öffnen und schließen lassen.
  2. Für toggleDoor() lassen sich nur eingeschränkt Unit Tests schreiben. Die Funktion interagiert nur mit dem globalen door-Objekt, und es gibt keine elegante Möglichkeit, die Funktion auf einem anderen Objekt wirken zu lassen – und sei es nur zu Testzwecken.
  3. Das Objekt door kann jederzeit und von allen möglichen Bestandteilen des Programms modifiziert werden. Wer weiß denn schon, welche Funktionen neben toggleDoor() noch an door herumfuhrwerken? Und wenn das Objekt einen fehlerhaften Wert eingenommen hat, welche der potenziell vielen Funktionen war dafür verantwortlich?

All diese Probleme sind auf die Verletzung der beiden Prinzipien zurückzuführen: die Funktion hat Nebenwirkungen, und die Daten im Programm können von x-beliebigen Funktionen verändert werden (was sich in JavaScript nicht verhindern lässt, aber wir könnten ja einfach darauf verzichten, Variablen zu modifizieren). Die Folge ist ein Programm, über das bei steigender Komplexität nur schwer zu räsonieren ist.

Unter Einhaltung der beiden Kernprinzipien könnte das Programm wie folgt geschrieben werden:

  1. var closedDoor = {
  2.   open: false
  3. };
  4.  
  5. function toggleDoor(door){
  6.   return { open: !door.open };
  7. }
  8.  
  9. var openedDoor = toggleDoor(closedDoor);
  1. toggleDoor() ist nicht mehr fest an das umgebende Programm gekoppelt, sondern kann isoliert für sich betrachtet werden – die Funktion kennt nur ihre Inputs und Outputs und kümmert sich nicht mehr um irgendwelche globalen Werte. So könnten wir die Funktion auch bequem von einem Projekt ins Nächste übernehmen und den gleichen Job machen lassen.
  2. toggleDoor() ist problemlos testbar. Wir können die Funktion mit allen möglichen Werten füttern und über ihren Rückgabewert feststellen, ob sie sich so verhält wie gewünscht.
  3. Es gibt keine wirklichen Variablen mehr, die aus unerfindlichen Gründen und zu komischen Zeitpunkten in unerwartete Zustände geraten. Technisch gesehen ist closedDoor natürlich noch immer eine Variable, aber statt sie zu modifizieren, erstellt toggleDoor() eine neue Tür im gewünschten Zustand. De facto sind alle Werte in unserem Programm also Konstanten und entsprechend einfach ist es, über ihr Zustandekommen zu räsonieren.

Funktionen höherer Ordnung

Wirklich Spaß macht funktionale Programmierung erst mit Funktionen höherer Ordnung. Dabei handelt es sich um Funktionen, die Funktionen als Parameter annehmen oder Funktionen, die Funktionen zurückgeben. Solche Funktionen können als Kombinations-Werkzeug verwendet werden, um aus vielen kleinen Einzel-Funktionen etwas völlig neues zu schaffen.

Ein Beispiel für eine Funktion höherer Ordnung ist in JavaScript die Array-Methode map(). In JS sind Funktionen und Methoden das Gleiche, nur dass Letztere auf Objekten existieren und operieren – so wie map() auf Arrays. Die Methode map() wird auf einem Array aufgerufen und nimmt als Parameter eine Funktion entgegen. Aus dem vorhandenen Array erzeugt map() ein neues Array, indem es die übergebene Funktion auf alle Elemente des Ausgangs-Arrays anwendet und deren Rückgabewerte in dem neuen Array ablegt. Beispiel:

  1. var values = [1, 2, 3];
  2.  
  3. var squares = values.map(function square(value){
  4.   return value * value;
  5. });
  6.  
  7. // Ergebnis: [ 1, 4, 9 ]

Dieser Code sieht vergleichsweise komplex aus, genügt aber immer noch unseren Prinzipien von funktionaler Programmierung: es werden keine Variablen verändert (die Werte von values und squares ändern sich nicht) und die beiden Funktonen square() und map() sind pure functions. Die Funktion square() hat ganz offensichtlich einen Input und einen Rückgabewert. Die Array-Methode map() hat mit ihrem Array und der Funktion square() zwei Eingabewerte und liefert als Rückgabewert ein neues Array.

Das Kombinieren von einfachsten Funktionen wie square() und map() hat neben den schon erwähnten Vorzügen von funktionaler Programmierung den großen Vorteil, dass die Bausteine unseres Programms sehr allgemein bleiben. Wie bei OOP können unsere Bausteine kombiniert und erweitert werden (bei OOP durch Vererbung, bei funktionaler Programmierung durch Funktionskomposition) und anders als bei OOP tendieren die Bausteine nicht dazu, so komplex und projektspezifisch zu werden, dass die Portabilität von Projekt zu Projekt leidet.

Programmieren wir doch mal einen Adventskalender! Als Hilfe holen wir uns die bekannte JavaScript-Library Lo-Dash ins Haus.

Das generelle Vorgehen beim funktionalen Programmieren könnte man mit dem Satz »Datentransformation durch Funktionskombination« beschreiben. Wir brauchen also Ausgangsdaten, die wir Stück für Stück in unseren Zielzustand übersetzen. Für den Adventskalender werden wir 24 Türen brauchen – ein Array mit den Zahlen 1 bis 24 sollte ein geeigneter Startwert sein. Ein solches Array können wir mit der Lodash-Funktion _.range() erzeugen:

  1. var days = _.range(1, 25);
  2. // ergibt das Array 1 - 24

Statt eines Arrays von Zahlen brauchen wir aber natürlich ein Array von Türchen. Doch wir wissen ja: von einem Array zum nächsten kommt man der map()-Methode:

  1. var days = _.range(1, 25);
  2.  
  3. var calendar = days.map(function createDoor(day){
  4.     return {
  5.       day: day,
  6.       open: false
  7.     }
  8.   });

Allerdings macht ein Adventskalender ohne offene Türen keinen Spaß. Mit jedem Tag muss ein Türchen mehr geöffnet werden – also brauchen wir eine Funktion, die einen veralteten Adventskalender ohne offene Türen durch einen neuen Kalender ersetzt (denn nicht vergessen, dass Daten nur ersetzt, nie geändert werden), bei dem die korrekten Türen geöffnet sind:

  1. var days = _.range(1, 25);
  2. var today = new Date().getDate();
  3.  
  4. var calendar = days.map(function createDoor(day){
  5.     return {
  6.       day: day,
  7.       open: false
  8.     }
  9.   });
  10.  
  11. function updateCalendar(calendar, day){
  12.   return calendar
  13.     .map(function openDoor(door){
  14.       var result = _.clone(door);
  15.       result.open = (door.day <= day);
  16.       return result;
  17.     });
  18. }
  19.  
  20. var currentCalendar = updateCalendar(calendar, today);

Wir könnten auch direkt in einem Schritt einen aktuellen Adventskalender ohne Zwischenstationen (und entsprechende Variablen) erstellen:

  1. var currentCalendar = _.range(1, 25)
  2.   .map(function createDoor(day){
  3.     return {
  4.       day: day,
  5.       open: false
  6.     }
  7.   })
  8.   .map(function openDoor(door){
  9.     var today = new Date().getDate();
  10.     var result = _.clone(door);
  11.     result.open = (door.day <= today);
  12.     return result;
  13.   });

Welche der beiden Formen man wählt, ist Geschmackssache – beide sind ohne großen Aufwand ineinander umbaubar. Die Vorteile von funktionaler Programmierung bleiben in beiden Fällen erhalten – jede einzelne Funktion übernimmt eine überschaubare Aufgabe, ist mangels Nebenwirkungen vom Rest des Programms sauber isoliert, bleibt portabel und die Werte als Zwischenschritte unseres Programmablaufs bleiben stets konstant. Daraus folgt ein weiterer Vorteil: zusätzliche Features (d.h. zusätzliche Datenwerte) lassen sich bequem erstellen, indem man vorhandene Werte entsprechend transformiert:

  1. var currentCalendar = _.range(1, 25)
  2.   .map(function createDoor(day){
  3.     return {
  4.       day: day,
  5.       open: false
  6.     }
  7.   })
  8.   .map(function openDoor(door){
  9.     var today = new Date().getDate()
  10.     var result = _.clone(door);
  11.     result.open = (door.day <= today);
  12.     return result;
  13.   });
  14.  
  15. var daysUntilChristmas = currentCalendar
  16.   .map(function(door){
  17.     return (door.open) ? 0 : 1;
  18.   })
  19.   .reduce(_.add)

Den Kalender (einem Array voller Türen) können wir zu einem Array von Zahlen transformieren: eine 0 für eine offene und eine 1 für eine noch geschlossene Tür. Dieses Array von Zahlen können wir auf eine Summe aufaddieren, indem wir die hierfür gedachte native Array-Methode reduce() hernehmen und sie mit der lediglich a - b rechnenden Addier-Funktion aus Lo-Dash füttern. Fertig ist die Anzahl der Tage bis Weihnachten!

Interaktive Elemente

Das ist alles schön und gut und wenn man möchte, kann man mathematisch oder auch praktisch nachweisen, dass man jedwedes Computerprogramm ausschließlich mit Funktionen bestreiten kann. Soweit wie wir es besprochen haben, handelt es sich dabei aber um eher langweilige Programme, denn es fehlt das interaktive Element! Wir transformieren ganz schön Daten von A nach B, aber wo bleiben Events und Ajax? Im Prinzip sind Events auch gar nicht so schwer in die funktionale Welt einzubringen – Events müssen nur in Objekte gekapselt werden, die eine API für Transformationen (d.h. Operationen wie map(), reduce()) bereitstellen. Das Paradigma hierfür nennt sich Functional Reactive Programming (FRP) und wird durch verschiedene Libraries wie RxJS und Bacon.js bereitgestellt.

FRP-Code funktioniert nach den gleichen Prinzipien wie herkömmliche funktionale Programmierung und verwendet das gleiche Vokabular, kann aber auf Events reagieren. Ein kleines Beispiel:

  1. // Event-Stream aus Button-Klicks
  2. Bacon.fromEventTarget($('button'), 'click')
  3.   // Stream zu 1 oder -1 transformieren
  4.   .map(function(evt){
  5.     return ($(evt.target).attr('id') === 'plus') ? 1 : -1;
  6.   })
  7.   // Akkumulator-Funktion, ähnlich wie reduce()
  8.   .scan(0, function(sum, value){
  9.     return sum + value;
  10.   })
  11.   // alert()-Methode auf window-Objekt mit Werten füttern
  12.   .assign(window, 'alert');

Klick-Events auf Buttons werden in einem Event Stream gekapselt und von dort aus zweimal transformiert. Bei der ersten Transformation wird das Event-Objekt in entweder 1 oder -1 verwandelt, je nachdem ob es einem Button mit der ID plus entsprang oder nicht (map()). Der zweite Transformationsschritt addiert alle aus dem Stream herauskommenden Zahlen auf, beginnend mit 0 (scan()). Jedes Mal, wenn ein Button geklickt wird, wird diese Transformationspipeline durchlaufen und der Zähler in scan() erhöht oder senkt sich. Zudem definiert der Code noch eine abschließende Nebenwirkung. Nebenwirkungen sind zwar etwas höchst unfunktionales, aber irgendwie müssen wir am Ende den aktuellen Zählerstand ausgeben – in diesem Fall sorgt assign() dafür, dass nach jedem Durchlauf der Transformationspipeline das jeweilige Endresultat an die alert()-Methode des Window-Objekts übergeben wird.

Mit Functional Reactive Programming ist wirkliche, praktische funktionale Programmierung möglich. Knifflig hieran ist einzig und allein, dass wir mit FRP wirklich gezwungen sind, funktional zu arbeiten – ein Abweichen von unseren eingangs definierten Prinzipien ist schlichtweg nicht mehr möglich. Das ist durchaus ein Vorteil, sobald man sich auf die funktionale Denke eingestellt hat. Im Einzelfall kann das etwas dauern (in meinem Fall waren es mehrere Wochen), doch nicht verzagen – es lohnt sich!

Kommentare

Markus Gans
am 20.12.2015 - 14:12

Hallo Peter, interessanter Artikel. Beim Lesen beschlich mich der Gedanke, daß durch das Vermeiden von wiederholtem Beschreiben von Variableninhalten, sich bei dieser Programmierart der Rechenaufwand drastisch erhöhen müßte. Somit dürfte unterm Strich, durch das mehr an zu bearbeitenden Code, die Performance der Anwendung darunter leiden. Das Prinzip der Unveränderbarkeit der Daten würde folglich in eine erhöhte Programmlaufzeit resultieren.
Aber auch die Lebensdauer der einzelnen Variablen dürfte sich hier zu Lasten des benötigten Arbeitsspeichers drastisch erhöhen.

Permanenter Link
Peter Kröner

Peter Kröner (Autor)
am 20.12.2015 - 15:30

Hallo und danke :)

Das (langsameres Programm durch statische Daten) dürfe eigentlich nicht passieren und wenn es passieren würde, wäre das meines Erachtens nicht automatisch ein Ausschlusskriterium.

Zum einen beinhaltet funktionales Programmieren auch das aufteilen des Programms in viele kleine Funktionen. Dadurch geraten Variablen regelmäßig out of scope (d.h. sie sind nicht mehr erreichbar) und die Garbage Collection kann den vormals belegten Speicher freigeben. Auch sollte man nicht die Optimierungsfähigkeiten moderner JS-Engines unterschätzen – die freuen sich bei der Ausführung des JS-Codes geradezu über statische Werte, speziell bei Objekten und deren Prototypen. Die Engines können diese dann nämlich in ihrer Programmrepräsentation in wirklich statische Werke verwandeln und entsprechende Performancegewinne verbuchen.

Und selbst wenn das nicht stimmen würde, müsste man immer noch abwägen, ob ein solcher (ggf. sehr kleiner) Performance-Nachteil nicht vielleicht ein angemessener Preis wäre, den man für ein übersichtlicheres Programm zu zahlen bereit ist. Permanenter Link

Die Kommentare sind geschlossen.