Einführung in die funktionale Programmierung

Funktionale Programmierung ist derzeit in aller Munde, da mittlerweile viele Mainstream-Sprachen funktionale Features haben. Doch ist das wirklich funktionale Programmierung? Auf der anderen Seite ist relativ unbekannt, dass viele Firmen «echte» funktionale Programmierung tatsächlich einsetzen, und das oft schon seit geraumer Zeit. Wirft man zum Beispiel einen Blick auf die Liste von Firmen, die Haskell kommerziell einsetzen, entdeckt man durchaus den einen oder anderen bekannten Namen.

Was bringt Firmen dazu, auf eine so ungewöhnliche Programmiersprache zu setzen? Um dieser Frage auf den Grund zu gehen, schauen wir uns einmal genauer an, was «funktionale Programmierung» eigentlich bedeutet.


Kurs: Einführung in die funktionale Programmierung

Im Workshop bietet Ihnen Nicole Rauch einen ersten Einblick in die funktionale Programmierung. Sie erfahren, warum sie immer wichtiger wird und lernen, funktionale Aspekte auch in Ihre tägliche Arbeit einfliessen zu lassen.

Alle Infos zum neuen Workshop


Die Kernaspekte funktionaler Programmierung

Immutability

Ein wichtiger Aspekt ist Immutability, d. h. einer Variablen darf nur einmal ein Wert zugewiesen werden. Viele funktionale Sprachen setzen diesen Aspekt konsequent um. Andere machen es zumindest schwierig, einmal gesetzte Werte später zu verändern. In Java z.B. gibt es keine direkte Unterstützung für Immutability. Jedoch ist es einfach, zumindest den einen Teil der Klassen eines Systems unveränderlich zu gestalten: Man kann gekapselte Attribute final und privat machen, sie nur im Konstruktor zuweisen und sämtliche Methoden so implementieren, dass sie nur lesend auf diese Attribute zugreifen. Vorteile bietet diese Vorgehensweise insbesondere bei der Parallelisierung: Concurrency-Probleme durch das quasi-gleichzeitige Modifizieren derselben Variablen aus verschiedenen Threads heraus sind damit unmöglich. Ein Beispiel dafür:

class Point {
  final private int x, y;

  public Point (int x, int y) {
    this.x = x;
    this.y = y;
  }

  // im restlichen Code werden x und y nur noch gelesen
}

Seiteneffektfreiheit

Als Seiteneffekt bezeichnet man alles, was den Ablauf eines Computerprogramms oder die Aussenwelt verändert, ohne von einer Funktion aus ihren Parametern berechnet worden zu sein. Wichtige Beispiele sind Ein-/Ausgabe, Exceptions, Logging, Abhängigkeit von (externen) Konfigurationen, Veränderungen des Zustands sowie Nichtdeterminismus (z.B. durch die Verwendung eines Zufallszahlengenerators).

In funktionalen Sprachen wird die seiteneffektfreie Programmlogik üblicherweise von seiteneffektbehaftetem Code getrennt. In manchen Sprachen ist es sogar aus der Typsignatur einer Funktion ersichtlich, ob diese einen Seiteneffekt produziert. Oder anders ausgedrückt: Normale Funktionen dürfen in diesen Sprachen gar keine Seiteneffekte haben.

Von derartigen Einschränkungen sind Sprachen wie Java meist weit entfernt. Trotzdem kann es sinnvoll sein, sich gewisse Codierungsregeln aufzuerlegen und die Bereiche, die seiteneffektbehaftet sind, von der seiteneffektfreien Kernlogik zu trennen. Dies kann z.B. innerhalb einer Klasse dadurch erfolgen, dass man seiteneffektfreie (und damit sehr einfach testbare) Methoden implementiert, die wiederum von seiteneffektbehafteten Methoden derselben Klasse aufgerufen werden. Es ist sogar unschädlich, diese seiteneffektfreien Methoden öffentlich zu machen, da sie ja keine Effekte auf die Klasse haben können, in der sie implementiert sind. In manchen Teams wird sogar dazu übergegangen, diese Methoden statisch zu machen, damit die Seiteneffektfreiheit noch deutlicher wird. Ein Beispiel für eine derartige Trennung:

class TrennungVonSeiteneffekten {
  public void mitSeiteneffekt(){
    String initialerWert = System.console().readLine();
    String ergebnis = ohneSeiteneffekt(initialerWert);
    System.out.println(„Das Resultat: “ + ergebnis);
  }

  public static String ohneSeiteneffekt(String initialerWert){
    return /* Ergebnis der Funktion */ ;
  }
}

Funktionen sind «first order citizens»

Ein weiterer wichtiger Aspekt ist die Tatsache, dass Funktionen «first order citizens» sind. Mit Funktionen kann man also dasselbe machen wie mit Zahlen oder Strings. Konkret:

  • Eine Funktion kann einer Variablen zugewiesen werden
  • Eine Funktion kann als Argument an eine Funktion übergeben werden
  • Eine Funktion kann von einer Funktion zurückgegeben werden

In Haskell sieht das folgendermassen aus:

-- Zuweisung einer Funktion:
times x y = x * y
timesVar = times
timesVar 3 5                                   -- liefert 15

-- Zuweisung eines Lambda-Ausdrucks:
times = (\ x y -> x * y)
times 3 5                                      -- liefert 15

-- Funktion an Funktion übergeben:
apply func arg = func arg
apply (\ x -> 3 * x) 5                         -- liefert 15

-- Funktion aus Funktion zurückgeben:
times x = (\y -> x * y)
times 3 5                                      -- liefert 15

Hier kommt die Haskell-Syntax für anonyme Funktionen, auch Lambdas genannt, zum Einsatz:

\ argumente -> funktionsrumpf

Der Schrägstrich wurde übrigens deswegen gewählt, weil er dasjenige ASCII-Zeichen ist, das dem griechischen Buchstaben λ am ähnlichsten sieht …

Currying

Dem aufmerksamen Leser mag aufgefallen sein, dass das Aufrufen einer Funktion mit einem Parameter, die eine Funktion mit einem Parameter zurückgibt, und das Aufrufen einer Funktion mit zwei Parametern in Haskell genau gleich aussieht. Wie kommt das? Dies liegt daran, dass Haskell, wie auch manch andere funktionale Sprache, nur Funktionen mit einem Parameter kennt. Definiert man eine Funktion, die mehrere Parameter zu haben scheint, so wird dies intern in eine Funktion umgewandelt, die einen Parameter nimmt und eine Funktion zurückliefert, die wiederum einen Parameter nimmt etc., bis alle Parameter versorgt sind. Dies nennt man Currying. Was zunächst nur wie ein technisches Detail klingt, hat durchaus einen praktischen Nutzen: Man kann eine Funktion durch Anwenden auf einen Teil ihrer Argumente spezialisieren und diese spezialisierte Funktion im weiteren Programmverlauf beliebig oft aufrufen – der Aufwand für die Spezialisierung muss hierbei nur einmal geleistet werden. Wir werden später noch ein Beispiel hierzu sehen.

Wichtige Bibliotheksfunktionen

Filter

Die Funktion filter nimmt eine Funktion f und eine Liste L und liefert eine Liste mit denjenigen Elementen von L, für die f den Wert true zurückliefert.

filter (\x -> x `mod` 2 == 0) [1,2,3,4]        -- liefert [2,4]

Map

Die Funktion map nimmt eine Funktion f und eine Liste L und liefert eine Liste, die die Ergebnisse des Anwendens der Funktion f auf jedes Element von L enthält.

map (\x -> x + 5) [1,2,3,4]           -- liefert [6,7,8,9]

Reduce

Die allgemeinste und damit flexibelste und mächtigste der hier vorgestellten Bibliotheksfunktionen ist reduce (in Haskell unter dem Namen foldl bekannt). reduce nimmt eine Funktion f, einen Startwert (auch Akkumulator genannt) und eine Liste L. Zunächst wird f mit dem Startwert und dem ersten Element von L aufgerufen. Der zurückgegebene Wert wird zusammen mit dem nächsten Element von L an f übergeben. Dies wird so lange fortgesetzt, bis alle Elemente von L verarbeitet wurden. Das Ergebnis der letzten Berechnung ist gleichzeitig das Ergebnis des gesamten reduce-Ausdrucks. Diese Verarbeitungskette wird in der folgenden Abbildung anschaulich dargestellt.

Ein Aufruf von reduce mit der Multiplikationsfunktion, dem Startwert 1 und der Liste [2, 3, 4, 5].
Ein Aufruf von reduce mit der Multiplikationsfunktion, dem Startwert 1 und der Liste [2, 3, 4, 5].

So sieht ein Beispiel in Haskell aus:

foldl (*) 1 [2,3,4,5]                          -- liefert 120

Eine einfache Berechnung

Bisher haben wir die Kernbestandteile funktionaler Programmierung kennengelernt. Nun wollen wir sie in einem praktischen Beispiel zur Anwendung bringen.

Angenommen, wir möchten die Summe der Quadrate der Zahlen von 1 bis 10 berechnen:

Quadratsumme

In «traditionellem» Java-Code würde man dies vermutlich in etwa so berechnen:

int sum = 0;
for(int i = 1; i <= 10; i++) {
  sum = sum + i * i;
}

An dieser Stelle möchte ich kurz die Welt der funktionalen Programmierung verlassen und gute Praktiken objektorientierter Programmierung in Erinnerung rufen. Relevante Stichworte sind hier «Clean Code» und das «Single Responsibility Principle». Letzteres besagt, dass jede Klasse, jede Methode und jede Codezeile genau eine Verantwortlichkeit haben sollte. Schauen wir uns also nochmal unsere obige Implementierung an. In diesen knapp drei Zeilen Java-Code sind so einige Verantwortlichkeiten versteckt:

  1. Erzeugen der Zahlenfolge von 1 bis 10
  2. Quadrieren einer Zahl
  3. Berechnen der Quadratzahl jeder Zahl in der Folge
  4. Addieren zweier Zahlen
  5. Aufsummieren der berechneten Quadratzahlen

Eine der Stärken funktionaler Programmierung ist das Trennen dieser Verantwortlichkeiten. In Haskell sieht das etwa so aus:

-- 1. Erzeugen der Zahlenfolge von 1 bis 10:
[1..10]

-- 2. Quadrieren einer Zahl:
\ x -> x * x

-- 3. Berechnen der Quadratzahl jeder Zahl in der Folge:
squaredSequence = map (\ x -> x * x) [1..10]

-- 4. Addieren zweier Zahlen:
(+)

-- 5. Aufsummieren der Quadratzahlen:
sum = foldl (+) 0 squaredSequence

Bei einem Ausdruck dieser Grösse verzichtet man üblicherweise auf die herausgezogene Variable squaredSequence, sodass sich folgender Code ergibt:

foldl (+) 0 (map (\x -> x*x) [1..10])

Hierbei fällt etwas unangenehm auf, dass man den Code quasi von rechts nach links lesen muss: Das Definieren der Liste der Zahlen von 1 bis 10 findet ganz rechts statt, links davon wird map zum Quadrieren verwendet, und wiederum links davon dient foldl zum Aufsummieren der Quadrate. Wem dies zu ungewohnt erscheint, der kann sich einen forward pipelining-Operator definieren, der das Ganze umdreht:

(>.>) x f = f x

Hier wird das Funktionsargument quasi von links in die Funktion «hineingeschoben», was für eine lesbarere Variante sorgt:

[1..10] >.> map (\x -> x*x) >.> foldl (+) 0

Hier sehen wir auch ein schönes Beispiel für Currying, denn sowohl map als auch foldl werden partiell evaluiert (map mit der Quadrierungsfunktion, foldl mit Additionsfunktion und Startwert), damit unser pipelining-Operator nur noch die Liste durchzureichen braucht.

Weiterführende Themen

Natürlich besitzen funktionale Programmiersprachen noch weitere Features, die sich nicht direkt in den heutigen Mainstream-Sprachen finden lassen. Typinferenz, Pattern Matching, algebraische Datentypen, Lazy Evaluation oder auch Monaden sind nur einige Beispiele dafür. Wer tiefer in die Thematik einsteigen möchte, kann auf ein reichhaltiges Angebot an Literatur, sowohl online als auch offline, zurückgreifen.

Fazit

Funktionale Programmierung wird immer wichtiger, und Aspekte funktionaler Programmierung haben inzwischen Einzug in viele nicht-funktionale Programmiersprachen gefunden. Dies erlaubt es, elegante neue Sprachkonzepte wie Lambdas oder diverse funktionale Bibliotheksfunktionen auch in Sprachen wie Java zu nutzen.

Doch es kommt nicht nur darauf an, durch die Programmiersprache gut unterstützt zu werden. Mindestens genauso hilfreich ist das Anwenden funktionaler Konzepte, selbst wenn es dafür keine entsprechende Sprachunterstützung gibt. Hierzu gehören die konsequente Verwendung von Immutabilität und die klare Isolierung von Seiteneffekten in bestimmte Codebereiche. Ist die überwiegende Menge des Codes seiteneffektfrei, werden die Vorteile wie grössere Verständlichkeit, leichtere Nachvollziehbarkeit, bessere Testbarkeit oder einfachere Parallelisierbarkeit sofort spürbar.

Wer sich intensiver mit der Materie auseinandersetzen möchte, findet hier Termine ganztägiger Workshops zum Thema.

Nicole Rauch
Nicole Rauch ist freiberufliche Softwareentwicklerin und Softwareentwicklungscoach mit umfangreichem Hintergrund in Compilerbau und formalen Verifikationsmethoden. In den letzten Jahren konzentrierte sie sich auf die Sanierung von Legacy Code Applikationen. Unabhängig davon ist die funktionale Programmierung ihre heimliche Liebe. Neben ihrer Entwicklertätigkeit wirkte sie an der Ausrichtung mehrerer selbstorganisierter Konferenzen und an der Initiierung der Softwerkskammer, einer User Community zum Thema Software Craftsmanship im deutschsprachigen Raum, sowie ihrer Karlsruher Regionalgruppe mit. In ihrer Freizeit ist sie an der Entwicklung der Softwerkskammer-Webplattform, einem node.js Projekt, beteiligt.

Diesen Artikel kommentieren

Wir sind sehr an einer offenen Diskussion interessiert, behalten uns aber vor, beleidigende Kommentare sowie solche, die offensichtlich zwecks Suchmaschinenoptimierung abgegeben werden, zu editieren oder zu löschen. Mehr dazu in unseren Kommentarregeln.

Kategorien

Letzte Beiträge

Nächste Kurse

Kurs: Technische Suchmaschinenoptimierung (SEO) für Fortgeschrittene («TECSEO»)

Workshop: Entwicklung von IoT-Lösungen mit Raspberry Pi, Eclipse und Java («IOTDEV»)

Kurs: DevOps Fundamentals («DEVFO»)

Kurs: .NET Core 2.0 und Visual Studio 2017 («CN7»)

Kurs: Einführung in die funktionale Programmierung («FUNCPR»)

Kurs: Agile Project Management™ – Foundation («AGF»)

Kurs: JavaScript für Softwareentwickler («JSFSEN»)

Kurs: Professional Scrum Master («AJS»)

Kurs: Grundlagen der Programmierung mit Java («JVP»)

Kurs: Einführung in die Programmierung mit Python («PYTHON»)