Funktionale Programmierung
Von Christoph Fabritz am 21.11.2016
Die Entwicklung von immer schnelleren CPUs ist an ihre Grenzen gestoßen. Dies liegt nicht nur an den grundlegenden, physikalischen Grenzen von Materialien sondern auch am Trend der ständigen Miniaturisierung und dem Fokus auf Energieeffizienz.
Um dennoch immer mehr Performance aus unserer Hardware herauszuholen, setzt man auf dessen Parallelisierung, d.h. mehr Kerne in unseren CPUs und GPUs.
Jeder der sich schon einmal an paraleller Programmierung versucht hat, wird wissen, dass das anfangs gar nicht so leicht ist. Das liegt daran, dass unsere Programmiersprachen zu einer Zeit entwickelt wurden, in der Arbeitsspeicher knapp und teuer war und CPUs alle 18 Monate doppelt so schnell wurden. Wir brauchten keine parallelen Algorithmen, um unsere Programme schneller laufen zu lassen. Alles was man brauchte war eine neue CPU. Die Last lag auf den Hardware-Designern, nicht auf den Software-Entwicklern.
Nun aber müssen wir als Programmierer selbst dafür sorgen, unsere Hardware optimal zunutzen. Dies können wir nicht, in dem wir Programme schreiben wie bisher, sondern wir müssen uns grundlegend neu orientieren. Hier kommt die funktionale Programmierung zum Tragen.
Das Haupthindernis der Pallelisierung ist „shared, mutable state“, also Zustand der zwischen Threads geteilt und verändert wird. Dies führt zu unvorhersehbaren Resultaten.
class Shape { private int width; private int height; Shape(int w, int h) { width = w; height = h; } public void setDim(w, h) { width = w; height = h; } } Shape s = new Shape(30, 20); s.setDim(10, 5); // A s.setDim(20, 55); // B println(s);
Dieser simple Code soll uns als Beispiel dienen. Was wird uns println in der Console anzeigen? Wenn wir beide setDim Aufrufe sequentiell abarbeiten „Shape{width=20, height=55}“, was für jeden verständlich ist. Wie sieht das Ergebnis jedoch aus, wenn wir setDim A und B gleichzeitig parallel ausführen? Nun, das können wir nicht mit Sicherheit sagen. Es kann jedes dieser folgenen Ergebnisse sein: „Shape{width=30, height=20}“, „Shape{width=10, height=20}“, „Shape{width=20, height=20}“, „Shape{width=30, height=5}“, „Shape{width=30, height=55}“, „Shape{width=10, height=5}“, „Shape{width=10, height=55}“, „Shape{width=20, height=55}“, „Shape{width=20, height=5}“. Es ist nicht nur unmöglich vorherzusagen, ob s wie setDim A oder B aussehen wird, auch haben wir zusätzlich „corrupt state“ indem s auch Zustände zwischen A und B annehmen kann. Dies erhalten wir durch mutable state und Funktionen mit Nebenwirkungen (setDim verändert Variablen außerhalb der Funktion).
Bei der funktionalen Programmierung verzichtet man auf veränderbare Zustände (mutable state) und Funktionen mit Nebenwirkungen (side effects). Dies klingt in erster Linie unmöglich, da wir gewohnt sind, ständig Variablen zuverändern.
class Shape { public final int width; public final height; Shape(int w, int h) { width = w; height = h; } public Shape setDim(w, h) { return new Shape(w, h); } } Shape s = new Shape(30, 20); Shape s2 = s.setDim(10, 5).setDim(20, 55); println(s); println(s2);
Schon durch kleine Veränderungen können wir unsere Klasse „thread-safe“ machen. Unsere Variablen sind nun alle Konstanten. Wollen wir unsere Klasse „verändern“ generieren wir in Wirklichkeit eine Kopie mit unserem neuen Zustand. Unabhängig davon, ob wir nun sequentiell oder parallel arbeiten, println(s) und println(s2) werden immer „Shape{width=30, height=20}“ und „Shape{width=20, height=55}“ in der Console anzeigen. Wir haben immutable state (unveränderliche Zustände) und Funktionen ohne Nebenwirkungen.
Dies ist nur eine kleine Kostprobe zur funktionalen Programmierung und nur eines der Themen, mit denen ich mich intensiv auseinander setze. Am Ende des Jahres angekommen kann ich sagen, dass 2016 für mich ein akademisch prägendes Jahr war. Unter anderem schrieb ich an Papers für IEEE-Konferenzen mit Kollegen von CSUN. Ich freue mich schon auf 2017, um mir weitere Ziele setzen zu können.
The comments are closed.