Funktionale Programmierung ist mehr als Java 8

Die wichtigsten Neuerungen von Java 8 sind Elemente aus der funktionalen Programmierung. Ermöglicht werden sie durch die neuen Lambda-Ausdrücke.

In Pocket speichern vorlesen Druckansicht 21 Kommentare lesen
Lesezeit: 21 Min.
Von
  • Michael Sperber
Inhaltsverzeichnis

Die wichtigsten Neuerungen von Java 8 sind Elemente aus der funktionalen Programmierung. Ermöglicht werden sie durch die neuen Lambda-Ausdrücke. Aus der Perspektive des Java-Programmierers bringen Lambdas und die neuen Streams wesentliche Verbesserungen mit. Aber wie schneidet Java 8 aus der Perspektive funktionaler Programmierer ab?

Viele Abstraktionen, die in der funktionalen Programmierung seit jeher vertraut sind, lassen sich in Java 8 nun mit vertretbarem Aufwand notieren. Deswegen ist Java 8 allerdings noch keine funktionale Programmiersprache. Systematische Abstraktion, persistente Datenstrukturen und manche in der funktionalen
Programmierung übliche höherstehende Abstraktionen sind nach wie vor die Domäne "richtiger" funktionaler Sprachen. Dieser Artikel beleuchtet den Aspekt der systematischen Entwicklung anhand konkreter Beispiele in Clojure und Scala und ihren Übersetzungen in Java 8.

Mehr Infos

Einer der größten Feinde nachhaltiger Softwareentwicklung ist die "Cut&Paste-Programmierung": Ein neues Stück Code soll etwas Ähnliches machen wie ein vorhandenes, also wird es kopiert und leicht verändert. Damit ist aber der gemeinsame Teil doppelt vorhanden – notwendige Änderungen daran sind fortan zweimal durchzuführen. Das probate Mittel gegen Cut&Paste-Programmierung ist die Abstraktion: Die Gemeinsamkeiten werden in einer Funktion (Prozedur, Methode ...) zusammengefasst und die Unterschiede durch Parameter ersetzt. Genau das ist die besondere Stärke funktionaler Sprachen, egal wie die Gemeinsamkeiten und Unterschiede aussehen.

Das zeigt ein kleines Beispiel in Clojure, das Statistiken von Fußball-Spielen auswertet. (Die relevanten Clojure-Elemente werden erläutert.) Dazu müssen die wichtigsten Daten eines Fußball-Spiels erst mal repräsentiert werden – Spieltag, Gastgeber, Toranzahl des Gastgebers, Gast, Toranzahl des Gasts. In Clojure geht das mit einer Record-Definition:

(defrecord Game
    [matchday home-team home-goals guest-team guest-goals])

Diese Form bestimmt eine Klasse Game mit den entsprechenden fünf Feldern. Außerdem wird ein Konstruktor namens Game. gleich mitdefiniert (der Punkt ist wichtig), der sich benutzen lässt, um die Spiele der Spielzeit 2009/2010 als Liste von Game-Objekten zu definieren:

(def season-2009-2010
  (list
   (Game. 1 "Wolfsburg" 2 "Stuttgart" 0)
   (Game. 1 "Mainz" 2 "Bayer 04" 2)
   (Game. 1 "Hertha" 1 "Hannover" 0)
   (Game. 1 "Bremen" 2 "Frankfurt" 3)
   (Game. 1 "Nürnberg" 1 "Schalke" 2)
   (Game. 1 "Dortmund" 1 "1. FC Köln" 0)
   (Game. 1 "Hoffenheim" 1 "Bayern" 1)
   (Game. 1 "Bochum" 3 "Gladbach" 3)
   (Game. 1 "Freiburg" 1 "Hamburg" 1)
  
   (Game. 2 "Frankfurt" 1 "Nürnberg" 1)
   ...))

Neben den Toren ist die wichtigste Kenngröße eines Spiels die Anzahl der Punkte beider Teams. Die folgende Funktion berechnet die Punkte des Gastgeber-Teams:

(defn home-points
  [g]
  (let [g1 (:home-goals g)
        g2 (:guest-goals g)]
    (cond
     (> g1 g2) 3
     (< g1 g2) 0
     (= g1 g2) 1)))

Die defn-Form bestimmt die Funktion home-points mit Parameter g (für das Game-Objekt). Die Funktion :home-goals ist der "Getter" des home-goals-Felds eines Game-Objekts, entsprechend :guest-goals. Die let-Form bindet die jeweilige Toranzahl an die Variablen g1 und g2; die cond-Form entspricht einer if-Kaskade in Java. (Zusammengesetzte Ausdrücke sind in Clojure immer in Präfix-Notation; entsprechend steht (> g1 g2) für g1 > g2 in Java.) Eine entsprechende Funktion gibt es auch für das Gästeteam:

(defn guest-points
  [g]
  (let [g1 (:guest-goals g)
        g2 (:home-goals g)]
    (cond
     (> g1 g2) 3
     (< g1 g2) 0
     (= g1 g2) 1)))

Da ist sie schon, die Cut&Paste-Programmierung. Die Funktionen home-points und guest-points
unterscheiden sich nur an den zwei Punkten, an denen jeweils :home-goals und :guest-goals stehen. In Clojure ist es einfach, über diese beiden Stellen zu abstrahieren:

(defn compute-points
  [goals-1 goals-2]
  (fn [g]
    (let [g1 (goals-1 g)
          g2 (goals-2 g)]
      (cond
       (> g1 g2) 3
       (< g1 g2) 0
       (= g1 g2) 1))))

An die Stelle der konkreten Getter :home-goals und :guest-goals sind die Parameter goals-1 und goals-2 getreten. Die neue Funktion compute-points akzeptiert diese und liefert dann selbst eine Funktion (erzeugt durch fn), die die gewünschte Punktezahl zurückliefert. "Wo kommt das fn her?", könnte man fragen. (defn f × ...) ist eine Abkürzung für (def f (fn × ...)).

Nun lassen sich home-points und guest-points mit compute-points definieren:

(def home-points (compute-points :home-goals :guest-goals))
(def guest-points (compute-points :guest-goals :home-goals))

Dieser Abstraktionsvorgang ist einfach und nahezu mechanisch und wird durch Clojures gleichförmige Lisp-artige Syntax zusätzlich unterstützt. Entwickler können so ihre Gehirnkapazität für die schwierigen Aufgaben der Programmierung aufsparen.