Vorteile der funktionalen Programmierung einfach erklärt

funktionale programmierung
© yasu0604mst - stock.adobe.com
Bewerten
6 Bewertungen,
Durchschnitt 3,7

Gegen funktionale Programmierung gibt es einige Vorbehalte. In diesem Artikel erfahren Sie, wie und im welchen Umfeld durch den Einsatz dieses Paradigmas die Verständlichkeit, Testbarkeit und Stabilität einer Software verbessert werden kann.

Funktionale Programmierung erlebt seit einigen Jahren eine Renaissance und ist trotzdem für viele Softwareentwickler immer noch ein Mythos. Sie wird als eine Welt aus eigentümlichen Begriffen, unkonventionellen Denkmustern und schwer lesbaren Quellcode wahrgenommen, welche sich eher für die Lösung theoretischer mathematischer Probleme als zur produktiven Erstellung sinnvoller Software eignet.

Lässt man sich allerdings von diesen Vorurteilen und der grauen Theorie nicht verschrecken, sondern lernt, welchen konkreten Einfluss diese Konzepte auf den erstellten Quellcode einer Software besitzen, wird man funktionale Programmierung als ein sehr einfach zu verstehendes und entwicklerfreundliches Programmierparadigma kennenlernen.

Dieser Artikel möchte den Begriff “funktionale Programmierung” praxisnah erklären, mit dem gängigen, objektorientierten Paradigma vergleichen sowie mit Codebeispielen in JavaScript dafür werben, mit funktionalen Programmiertechniken einfache und stabile Software zu schreiben.

Was ist funktionale Programmierung?

Als Antwort auf diese Frage findet man in der Fachliteratur oft eine Vielzahl mehr oder weniger gut verständlicher Erklärungen, oftmals hinterlegt mit Beispielen aus der Mathematik. Der für die Softwareentwicklung wirklich relevante Aspekt lässt sich jedoch in einem einfachen Satz beschreiben:

Funktionale Programmierung ist die Erstellung von Software durch die Verwendung von Pure Functions (reinen Funktionen).

Eine Pure Function ist eine ganz normale Programmfunktion, die lediglich zwei besondere Eigenschaft hat:

  1. Für die gleichen Eingabeparameter wird stets der gleiche Rückgabewert geliefert
  2. Es existieren keine Side-Effects (Nebenwirkungen)

Durch diese beiden Merkmale ist eine Pure Function vergleichbar mit einer schlichten mathematischen Funktion y = f(x), die für denselben x-Wert immer denselben y-Wert ermittelt. Das bedeutet in der Praxis, dass die gesamte Programmlogik einer Funktion allein mit den Daten aus den Eingabeparametern arbeitet und als Ergebnis genau ein Rückgabewert entsteht. Während des Funktionsaufrufes entstehen keinerlei Side-Effects, wie beispielsweise das Verändern des Zustandes von Variablen außerhalb der Funktion oder der Eingabeparameter, das Schreiben in die Datenbank bzw. einer Datei oder der Aufruf anderer Funktionen, die selbst Side-Effects besitzen.

Unveränderlichkeit von Daten

Die wesentlichste Voraussetzung für die Erstellung von Pure Functions ist, dass bei einem Aufruf keinerlei Daten durch die Funktion verändert (manipuliert) werden dürfen. Dies ist vor allem für die Datenstrukturen, die als Eingabeparameter verwendet werden, zu beachten. Jegliche Veränderung an diesen Daten wird durch die Erstellung und Rückgabe neuer Daten, welche die Änderung beinhalten, realisiert.

Keinen Shared-State

Aufgrund dessen, dass Pure Functions ausnahmslos auf den Daten der Eingabeparameter operieren und diese auch nicht verändern, gibt es zwischen verschiedenen Pure Functions keinen Shared-State (gemeinsamen Zustand). Die Funktionen arbeiten somit autark. Solange die Eingabeparameter beim Funktionsaufruf gleichbleiben, spielt die Aufrufreihenfolge der Funktionen von außen gesehen keine Rolle für das Endergebnis.

Higher-Order Functions

In vielen funktionalen Programmiersprachen sind Funktionen normalen Daten gleichgesetzt, indem sie einfach auf einer Variable gespeichert und herumgereicht werden können. Ist dies der Fall, werden Funktionen als First-Class Objects (Objekte erster Klasse) bezeichnet und können von anderen Funktionen als Eingabeparameter und Rückgabewert verwendet werden. Funktionen, welche mindestens eine Funktion als Eingabeparameter erwarten oder als Rückgabewert liefern, werden Higher-Order Functions (Funktion höherer Ordnung) genannt. Das Gegenteil sind First-Order Functions (Funktion erster Ordnung), die keine dieser Eigenschaften besitzen.

 

Was sind die Vorteile der funktionalen Programmierung?

Das Konzept der Pure Function lässt sich nicht nur relativ leicht erklären, sondern ermöglicht in der Praxis Software zu schreiben, die vom Entwickler einfach zu verstehen ist, da die einzelnen Funktionen untereinander keine Abhängigkeiten besitzen und für sich allein geschrieben, verstanden und getestet werden können. Dies ist ein riesiger Vorteil.

Ein Großteil der heute geschriebenen Software folgt jedoch einem anderen Paradigma, dem objektorientierten Modell (OO-Modell), welches genau das Gegenteil propagiert. In der objektorientierten Welt werden Funktionen als Methoden bezeichnet und zusammen mit den dazugehörigen Daten von Objekten (Klassen) gekapselt. Beim Aufruf einer Methode kann diese sowohl auf die übergebenen Eingabeparameter als auch auf die Daten des Objektes selbst zugreifen. Dabei wird bewusst der Zustand eines Objektes über die entsprechenden Methoden manipuliert, es werden also die Daten im Objekt selbst verändert. Zusätzlich werden die Daten eines Objektes als Rückgabewert eines Methodenaufrufes an andere Objekte weitergereicht und unterliegen dort der weiteren Manipulation.

Dieses Verhalten wird bei der objektorientierten Programmierung bewusst in Kauf genommen, da sich durch dieses Modell Elemente aus der realen Welt viel einfacher in Software abbilden lassen. Allerdings steigt mit zunehmender Größe die Komplexität von objektorientierter Software enorm. Der Aufruf einer Methode kann an unterschiedlichen Stellen zu Zustandsänderungen führen, welche wiederum nachfolgende Zustandsänderungen beeinflussen. Es ist der Normalzustand, dass ein wiederholter Methodenaufruf mit identischen Parametern zu einem anderen Ergebnis führt. Für einen Entwickler ist es in einem solchen System viel schwieriger, Abläufe zu verstehen und korrektes Verhalten vorherzusagen. Die Fehleranfälligkeit der Software steigt.

In der funktionalen Programmierung ist es durch die Verwendung von Pure Functions viel einfacher und intuitiver, den Programmablauf des Quellcodes nachzuvollziehen und zu prognostizieren. Anwendungen, die nach diesem Paradigma geschrieben werden, besitzen automatisch ein relativ hohes Maß an loser Kopplung, d.h. Änderungen in einem Teil haben nur geringe Auswirkungen auf andere Teile der Software. Solange die Aufrufparameter und der Rückgabewert identisch bleiben, kann die Implementierung einer Pure Function beliebigen Änderungen unterliegen, ohne dass andere Codestellen davon betroffen sind. Des Weiteren lassen sich mehrere dieser Funktionen sehr gut zu neuen Funktionen kombinieren, da keinerlei Nebenwirkungen bei den Aufrufen beachtet werden müssen. Zusätzlich besitzen funktional geschriebene Programme die Eigenschaft der referential Transparency (referenziellen Transparenz). Dies bedeutet, dass im Fall von konstanten Aufrufparametern der komplette Funktionsaufruf im Programm durch die Rückgabewerte ersetzt werden kann, ohne dass sich eine Verhaltensänderung für das Gesamtsystem ergibt.

 

Wofür eignet sich funktionale Programmierung?

Die Vorteile der funktionalen Programmierung, vor allem im Vergleich zur Objektorientierung, liegen auf der Hand. In der Praxis ist es dagegen im Normalfall nicht möglich, ein komplettes Softwaresystem rein funktional zu entwickeln. Es wird immer Quellcode-Bereiche geben, in denen der gemeinsame Zustand und Nebenwirkungen eine wichtige Rolle spielen. Wenn ein Softwaresystem mit seiner Umgebung kommuniziert, z.B. mit Datenbank, Dateisystem oder beim Versenden von Daten über ein Netzwerk, ist das Paradigma der Pure Functions völlig ungeeignet.

Wie bereits beschrieben, hat die objektorientierte Programmierung ihren Vorteil bei der softwaretechnischen Umsetzung von realen Objekten. Ihre Stärke liegt in dem Erstellen von Strukturen und der Architektur eines Softwaresystems, da in diesem Paradigma Daten und Methoden eine Einheit bilden. Aus diesem Grund wird sie in der Praxis oftmals zurecht der funktionalen Programmierung vorgezogen. Allerdings passiert dies auch dort, wo sie eindeutig im Nachteil ist. Denn wenn es um die konkrete Verarbeitung von Daten bzw. um die eigentliche Business-Logik einer Anwendung geht, spielt die funktionale Programmierung offenkundig ihre Stärke aus. Bei zunehmender Komplexität der Algorithmen steigt die Wichtigkeit, leicht lesbaren, prägnanten und gut testbaren Code zu schreiben. In diesen Bereichen ist der Einsatz von Pure Functions der objektorientierten Programmierung weit überlegen und sollte von den Entscheidungsträgern in Erwägung gezogen werden.

Die größten Vorteile bietet die funktionale Programmierung vor allem beim Thema Nebenläufigkeit und verteilter Programmierung. Wenn Daten zeitgleich von parallel ausgeführten Softwareprozessen verarbeitet werden, ist es wichtig sicherzustellen, dass die Algorithmen die gemeinsam genutzten Daten nicht gegenseitig verändern und damit das Gesamtsystem womöglich in einem inkonsistenten Zustand versetzen. Fehler, welche durch parallele Programmausführung entstehen, sind vom Entwickler schwer nachzuvollziehen und somit aufwendig in der Behebung. Aufgrund der Eigenschaft, dass Pure Functions niemals Daten verändern und keinerlei Nebenwirkung aufweisen dürfen, sind sie im Bereich der Nebenläufigkeit das Mittel der Wahl, um die Fehleranfälligkeit zu reduzieren.

Funktionale Programmierung mit Beispielen in JavaScript

Für die praktische Umsetzung der funktionalen Programmierung gibt es eine Vielzahl von reinen, funktionalen Programmiersprachen, oftmals mit akademischem Hintergrund wie z.B. Lisp oder Haskell. Allerdings ist für die Anwendung von funktionalen Programmierparadigmen keine reine funktionale Sprache notwendig. Selbst mit weit verbreiteten objektorientierten Programmiersprachen wie Java, C# oder JavaScript kann in gewissem Umfang funktional Software entwickelt werden. Der wesentlichste Unterschied zu rein-funktionalen Programmiersprachen ist dabei, dass der Entwickler für die Einhaltung der funktionalen Prinzipien (Erstellen von Pure Functions, Unveränderlichkeit der Daten, keine Side-Effects) selbst verantwortlich ist und nicht durch Eigenschaften der Programmiersprache dazu gezwungen wird.

JavaScript ist zurzeit eine der beliebtesten Programmiersprachen und die Standardsprache für die Webentwicklung. Sie ist vom Ursprung her objektorientiert, erfüllt aber alle Voraussetzungen, um als funktionale Sprache eingesetzt zu werden. Vor allem ab Version 6 (standardisiert als ECMAScript 6) bietet JavaScript dem Entwickler eine Vielzahl von Möglichkeiten für eine effiziente, funktionale Programmierung. In den folgenden Abschnitten werden beispielhaft die gängigsten funktionalen Programmiermuster in JavaScript beschrieben.

Funktionen als First-Class Object

Funktionen existieren in JavaScript nicht nur unabhängig von Objekten, sondern erfüllen alle Eigenschaften eines First-Class Objects, d.h. sie können auf einer Variable gespeichert und als Parameter oder Rückgabewert eines Funktionsaufrufes verwendet werden.

const duplicate = function(number) {

    return number * 2;

};

const numbers1 = [1,2,3];

const numbers2 = numbers1.map(duplicate);

//Inhalt von numbers1

//[1,2,3]

//Inhalt von numbers2

//[2,3,6]

In dem Beispiel wird auf die Variable duplicate eine Funktion gespeichert, die nichts Anderes macht, als eine übergebene Zahl zu verdoppeln und das Ergebnis zurückzugeben. Anschließend wird ein Array numbers1 mit den Zahlen 1,2,3 erstellt und auf diesem die map()-Methode aufgerufen. Sie bekommt die duplicate()-Funktion als Eingangsparameter übergeben und ruft diese für jede einzelne Zahl in numbers1 auf. Der Rückgabewert der map()-Methode ist ein neues Array numbers2, welche die Ergebnisse jedes einzelnen Funktionsaufrufes von duplicate() enthält, also die Zahlen 2,3,6.

Die Funktion duplicate() ist ein gutes Beispiel für eine Pure Function. Die map()-Methode ist dagegen nicht “pure”, da sie Bestandteil des Array-Objektes ist und auf den Daten des Arrays operiert. Dies ist hier aber nur von theoretischer Bedeutung. Entscheidend dagegen ist, dass map() das bestehende Array numbers1, auf dem Sie aufgerufen wird, nicht verändert, sondern ein neues Ergebnis-Array erstellt.

Higher-Order Functions

Die map()-Methode im vorhergehende Beispiel erwartet als Eingangsparameter eine Funktion und gehört demzufolge zu der Kategorie der Higher-Order Functions. Deren Potential kommt vor allem dann zur Geltung, wenn wie im folgenden Beispiel, eine neue Funktion als Rückgabewert geliefert wird.

const multiplyBy = function(factor) {

    return function(number) {

        return number * factor;

    };

};

const multiplyBy5 = multiplyBy(5);

const numbers1 = [1,2,3];

const numbers2 = numbers1.map(multiplyBy5);

//Inhalt von numbers2

//[5,10,15]

Die Funktion multiplyBy() akzeptiert als Eingangsparameter eine beliebige Zahl und verwendet diese als Faktor für eine neu erstellte Funktion, welche eine beliebige Zahl erwartet und diese mit dem Faktor multipliziert. Im Beispiel wird multiplyBy() mit der Zahl 5 aufgerufen. Als Rückgabewert entsteht eine Funktion, die jede übergebene Zahl mit 5 multipliziert und das Ergebnis zurückgibt. Wendet man diese Funktion mit Hilfe von map() auf das Array numbers1 an, erhält man ein neues Array mit den Werten [5,10,15].

JavaScript besitzt für das Erstellen von Funktionen neben der Verwendung des Schlüsselwortes function noch eine Kurzschreibweise, welche auf Grund der verwendeten Syntax als Arrow-Function-Definition bezeichnet wird. Sie hat für die funktionale Programmierung eine hohe Bedeutung, da sie sehr kurze und prägnante Funktionsdefinitionen erlaubt. Allerdings kann für Softwareentwickler, die diese Notation nicht gewöhnt sind, der Anblick eines so beschriebenen Quellcodes auf den ersten Blick befremdlich wirken. Ist diese Phase aber erst einmal überwunden, lassen sich komplexe Sachverhalte deutlich schneller erfassen. Die Funktion multiplyBy() wird bei Verwendung der Arrow-Function-Definition genau eine Zeile umfassen:

const multiplyBy = factor => number => number * factor;

const multiplyBy5 = multiplyBy(5);

const numbers1 = [1,2,3];

const numbers2 = numbers1.map(multiplyBy5);

//Inhalt von numbers2

//[5,10,15]

Closures

Das vorhergehende Beispiel demonstriert eines der wichtigsten Konzepte der funktionalen Programmierung, das sogenannte Closure. Wird in JavaScript eine Funktion erstellt, wird neben der Funktion auch deren Erstellungskontext (die Variablen, welche zu diesem Zeitpunkt existieren) mit abgespeichert. Auf den Inhalt dieses Kontextes kann die Funktion während ihrer Ausführung zugreifen. In dem Beispiel von multiplyBy() umfasst der Erstellungskontext die Variable factor, welche die neu erstellte Funktion für die Multiplikation verwendet.

Closures sind ein mächtiges Feature in JavaScript, welche aus diesem Grund mit hohen Verantwortungsbewusstsein vom Entwickler verwendet werden müssen. Falsch angewandte Closures können eine Pure Function ganz schnell in eine Unpure Function verwandeln, wenn Variablen im Erstellungskontext nachträglich verändert werden. Aus diesem Grund ist die Unveränderlichkeit der Daten eine der wichtigsten Prämissen in der funktionalen Programmierung.

Currying

Die Anzahl der Eingabeparameter einer Funktion wird Arity (Stelligkeit bzw. Arität) genannt. Um die Komplexität einer Funktion zu verringern und die Wiederverwendbarkeit zu erhöhen, ist die Anzahl der Parameter möglichst gering zu halten. Das Optimum ist eine Funktion mit genau einem Eingabeparameter (Unary Function), was oftmals in der Praxis nicht ohne Weiteres umsetzbar ist. In der funktionalen Programmierung existiert eine Technik mit dem Namen Currying, welche Funktionen mit mehreren Parametern in eine Sequenz von Unary Functions umwandelt. Im nachfolgenden Codebeispiel haben wir eine Funktion, welche die drei Zahlen a,b,c erwartet und deren Summe als Rückgabewert liefert:

const add3Numbers = (a,b,c) => a + b + c;

const result = add3Numbers(2,5,9);

// result ist 16

Das selbe Beispiel mit ausschließlich Unary Functions sieht wie folgt aus:

const add3Numbers = a => b => c => a + b + c;

const result = add3Numbers(2)(5)(9);

// result ist immer noch 16

Die Currying-Version von add3Numbers() ist jetzt eine Abfolge von drei Funktionsaufrufen, mit jeweils einem Eingabeparameter. Dabei geben die ersten beiden Funktionsaufrufe jeweils eine neue Funktion zurück, welche direkt mit dem nächsten Parameter aufgerufen wird. Der Vorteil von Currying wird auf den ersten Blick in dem stark vereinfachten Beispiel wahrscheinlich nicht erkennbar sein. In der Praxis ermöglicht diese Technik, dass Funktionsaufrufe mit ihrer Parameterreihenfolge zwischengespeichert und anschließend mit unterschiedlichen Parametern weiter aufgerufen werden können. In dem dargestellten Beispielcode könnte das Zwischenergebnis von add3Numbers() auch als neue Funktion add7() gespeichert werden, welche zu einer übergebenen Zahl 7 addiert:

const add3Numbers = a => b => c => a + b + c;

const add7 = add3Numbers(2)(5);

const result1 = add7(5);

const result2 = add7(8);

// result1 ist 12

// result2 ist 15

Functional Composition

Gutes Softwaredesign zeichnet sich dadurch aus, dass ein komplexes Softwaresystem aus einer Vielzahl von kleinen und einfachen Komponenten besteht, die unabhängig voneinander erstellt und getestet werden können und möglichst einen hohen Grad an Wiederverwendung aufweisen. Für die funktionale Programmierung bedeutet dies, dass eine Vielzahl von simplen und spezialisierten Funktionen erstellt und an unterschiedlichen Stellen zu komplexen Funktionen orchestriert werden. Eine Möglichkeit dazu bietet die funktionale Komposition (functional composition), bei der eine Eingabevariable mehrere Funktionen nacheinander durchläuft und somit eine komplexe Operation entsteht. Das nachfolgende Codebeispiel fasst alle bisherigen Beispiele zusammen. Die Funktion doComplexOperation() ist eine Komposition aus den Funktionen add7() und multiplyBy5():

const multiplyBy = factor => number => number * factor;

const multiplyBy5 = multiplyBy(5);

const add3Numbers = a => b => c => a + b + c;

const add7 = add3Numbers(2)(5);

const doComplexComputation = x => multiplyBy5(add7(x));

const numbers1 = [1,2,3];

const numbers2 = numbers1.map(doComplexComputation);

//Inhalt von numbers2

//[40, 45, 50]

Wird die Funktion doComplexOperation() mit Hilfe von map() auf das Array numbers1 angewendet, wird jede Zahl in numbers1 zuerst mit 7 addiert und dann mit 5 multipliziert. Das Ergebnis ist ein neues Array mit den Zahlen 40, 45, 50.




Fazit

Das Ziel der funktionalen Programmierung ist es, den Prozess der Softwareentwicklung für die beteiligten Entwickler einfach und verständlich zu machen. Auf Grund des akademischen Hintergrundes und der Verwendung einer eigenen Begriffswelt existieren aber immer noch große Vorurteile gegenüber der konkreten Anwendbarkeit in Softwareentwicklungsprojekten. Dabei geht es bei der funktionalen Programmierung ausschließlich um die Erstellung von Pure Functions. Ist die Bedeutung dieses Grundgedankens erst einmal vom Entwickler verstanden und verinnerlicht, lassen sich die notwendigen Techniken und Fähigkeiten zur Anwendung der funktionalen Programmierung relativ einfach erlernen. Wird die Herausforderung, geeignete Teile einer Software funktional zu entwickeln, angenommen, werden die Vorteile in Bezug auf Verständlichkeit, Testbarkeit und Stabilität im erstellen Quellcode sichtbar werden.

FAQ

Was ist funktionale Programmierung?

Als funktionale Programmierung bezeichnet man die Erstellung von Software durch die Verwendung von Pure Functions (reinen Funktionen). Eine Pure Function ist eine Programmfunktion mit zwei Eigenschaften: Für die gleichen Eingabeparameter wird stets der gleiche Rückgabewert geliefert und es existieren keine Side-Effects (Nebenwirkungen).

Warum funktional programmieren?

Das Konzept ermöglicht die Entwicklung einfach zu verstehender Software, in der die einzelnen Funktionen voneinander unabhängig sind und für sich allein geschrieben, verstanden und getestet werden können. Das hat in einigen Fällen starke Vorteile. Dennoch stößt funktionale Programmierung wegen eigentümlicher Begriffe, unkonventionellen Denkmustern und einem schwer lesbaren Quellcode häufig auf Vorbehalte.

Welche sind die Einsatzgebiete für die funktionale Programmierung?

Funktionale Programmierung spielt ihre Stärke aus, wenn es um die konkrete Verarbeitung von Daten oder um die eigentliche Business-Logik einer Anwendung geht. Bei zunehmender Komplexität der Algorithmen steigt die Wichtigkeit, leicht lesbaren, prägnanten und gut testbaren Code zu schreiben. In diesen Bereichen ist der Einsatz von Pure Functions der objektorientierten Programmierung weit überlegen.

Keine Updates mehr verpassen: 1
© yasu0604mst - stock.adobe.com

Zurück