Frege – ein Haskell für die JVM

Funktional zu programmieren, kommt gerade wieder groß in Mode. Die Ideen sind nicht neu, aber gerade für uns an Objektorientierung gewöhnte Java-Entwickler sehen sie erst mal ziemlich kompliziert aus. Dabei versprechen die Verfechter der funktionalen Programmierung, dass man besser strukturierte und damit besser lesbare Anwendungen schreiben kann. Hinzu kommt, dass man bei rein funktionalen Sprachen Seiteneffekte komplett vermeiden kann und somit beste Voraussetzungen schafft, nebenläufig zu programmieren. Seiteneffektfrei heißt, dass kein globaler Zustand verändert wird, was uns Entwicklern zudem das Verständnis für den Programmcode erleichtert. Seiteneffektfrei bedeutet auch, dass man die Funktionen immer wieder aufrufen kann und für die gleichen Input-Parameter auch dieselben Ergebnisse erhält. Das ermöglicht verschiedenste Optimierungsmöglichkeiten, wie zum Beispiel das Cachen von Funktionsaufrufen.

Als Java-Entwickler waren wir funktionale Programmierung bisher nicht gewöhnt. Mit Java 8 hat sich mit der Einführung von Streams, Default-Methoden in Interfaces und Lambda-Ausdrücken zwar einiges geändert. Trotzdem können wir in Java Seiteneffekte nur durch eigene Initiative vermeiden. Die Sprache unterstützt uns hier nicht, mal von einigen immutable Datentypen (String, Integer, …) abgesehen. Java bleibt eine imperative Sprache, die dazu verleitet, Zustände zwischenzuspeichern und Entwickler in Zuweisungen und Statements denken zu lassen. Dabei muss man sich von der Java-Plattform gar nicht wegbewegen, um bessere Ansätze in funktionalen Sprachen kennenzulernen. Es gibt ja mittlerweile hunderte von alternativen Sprachen für die uns vertraute JVM. Auch wenn nur ein Teil praktische Relevanz hat, gibt es doch gerade bei dieser Handvoll Alternativen interessante Ansätze für die funktionale Programmierung. Zu nennen wären da unter anderem Groovy, Scala und Clojure. Unterscheiden kann man diese Sprachen zum einen nach objektorientiert (Groovy), funktional (Clojure) und objektfunktional (Scala) oder auch nach der Typisierung, die bei Groovy und Clojure dynamisch und bei Scala statisch ist (Groovy kennt mittlerweile auch statische Typisierung). Allen gemeinsam ist die Möglichkeit, mit Java Code zu interagieren und so beispielsweise Zugriff auf die große Menge an Java Bibliotheken und Frameworks zu ermöglichen.

Vor noch nicht allzu langer Zeit hat sich mit Frege ein weiterer Kandidat dazugesellt. Das besondere ist, dass es sich um eine Haskell-Implementierung handelt. Haskell ist meiner Meinung nach die funktionale Programmiersprache mit der aktuell höchsten Aufmerksamkeit. Zudem stammt sie aus dem akademischen Umfeld, und somit konnte man ohne Zwänge der Industrie einiges mal ganz einfach richtig umsetzen und musste keine Kompromisse eingehen.

Haskell hat einerseits viele Elemente von anderen Sprachen übernommen, aber auch eigene Trends geprägt. Es handelt sich um eine reine (pure) funktionale Sprache, wobei Funktionen eben keine Seiteneffekte produzieren. Das bedeutet, dass das Ergebnis einer Funktion nur von den Eingabe-Parametern abhängt und nicht wann und wie oft sie aufgerufen wurde. Den Zugriff auf Ein-/Ausgabeoperationen oder zustandsbehaftete Berechnungen (z. B. die Ermittlung der Systemzeit) muss man durch Monaden von den pure Functions isolieren. Die Typisierung ist übrigens statisch und der Compiler kann durch Typinferenz einen Großteil der Typinformationen aus dem Kontext ermitteln. Damit können typische Programmierfehler bereits zur Compilezeit gefunden werden, etwas das viele an Java schätzen (und dynamisch typisierte Sprachen lieber meiden).

Besonderheiten von Haskell sind unter anderem die Lazy Evaluation (Bedarfsauswertung von beispielsweise endlosen Folgen), Mustervergleiche (Pattern Matching), Lambdaausdrücke (für Funktionen höherer Ordnung), Currying und partielle Funktionsanwendung, Typvariablen und algebraische Datentypen. Einiges davon wollen wir in diesem und in weiteren Blog-Beiträgen genauer unter die Lupe nehmen. Natürlich werden wir auf der JVM bleiben und entsprechend Frege als Haskell-Implementierung verwenden.

Leider gibt es noch einige kleine Unterschiede, in denen Frege von seinem Vorbild abweicht. Der Hintergrund ist, dass Frege als praktikable JVM-Sprache entworfen wurde, wobei die Interoperabilität mit Java und anderen JVM-Sprachen im Vordergrund stand. Man hat zwar versucht, möglichst nah an das Vorbild Haskell heranzukommen, aber durch die Verwendung elementarer JVM-Datentypen (boolean, String) und Low-Level System Funktionen, ergeben sich letztlich doch diverse Unterschiede. Core-Committer Dierk König hat aber in einem Interview auf der JavaOne kürzlich angekündigt, dass man mit der nächsten Version wieder einige der Unterschiede ausmerzen möchte.

Auch wenn für Frege eine Read Eval Print Loop und Unterstützungen für Entwicklungsumgebungen (Eclipse, IntelliJ) existieren, kann man zunächst komplett ohne irgendwelche Installationen loslegen und einfach die Online-REPL nutzen:

http://try.frege-lang.org/

Das obligatorische Hello World ist eigentlich total untypisch, weil es durch die Konsolen-Ausgabe einen IO-Seiteneffekt produziert.

frege> putStrLn "hello world"
()
hello world

Schauen wir uns also lieber pure Funktionen an. Und dazu zählt das Addieren von Zahlen, wofür wir den Plus-Operator (+) verwenden können.

frege> 1 + 1
2

Die für uns aus imperativen Sprachen gewöhnte Infix-Notation ist gar kein typischer Funktionsaufruf, sondern vielmehr syntaktischer Zucker, um binäre Operationen (mit genau zwei Operanden) verständlicher zu machen. Eigentlich typisch für Haskell und viele andere funktionale Programmiersprachen ist die Postfix-Notation, wobei der Operator/die Funktion als erstes kommt und danach die Parameter aufgelistet werden. Die Parameter werden übrigens nicht mit einem Klammerpaar umschlossen, wie wir das von imperativen Sprachen gewohnt sind. Die allgemeine Form in Postfix-Notation sieht in Haskell/Frege also folgendermaßen aus, wobei x, y und z Parameter sind:

func x y z

Operatoren müssen in diesem Fall allerdings in Klammern angegeben werden, weil sie sonst nicht eindeutig als Funktionsaufrufe interpretiert werden können.

frege> (+) 1 1
2

Haskell und auch Frege sind statisch typisiert, wobei der Compiler in den meisten Fällen den Typ automatisch inferiert. Das heißt, alles hat einen Typ, auch wenn wir ihn nicht explizit angeben. Mittels :type oder :t kann man den Typ eines Ausdrucks ermitteln:

frege> :type 1 + 1
Int

Das funktioniert auch bei Funktionen. Dazu definieren wir uns zunächst mal eine einfache:

frege> addOne x = x + 1
function addOne :: Int -> Int
frege> :t addOne
Int -> Int
frege> addOne 2
3

Durch unsere Implementierung der addOne-Funktion wird impliziert, dass ein Eingabeparameter vom Typ Integer erwartet und wiederum ein Integer als Ergebnis zurückgeliefert wird (Int -> Int). Wenn man diese Funktion mit etwas anderem als einer Ganzzahl aufruft, meldet uns bereits der Compiler die fehlerhafte Verwendung:

frege> addOne True
4: H <console>.fr:4: supertypes of boolean:
4: H <console>.fr:4: does not contain int
4: E <console>.fr:4: type error in expression true
type is : Bool
expected: Int

In Frege/Haskell haben Funktionen interessanterweise nur einen Parameter. Wie man trotzdem mehrere Argumente definieren kann, werden wir uns im nächsten Teil dieser Reihe anschauen.

Short URL for this post: http://wp.me/p4nxik-2t2
This entry was posted in Java and Quality, Other languages for the Java VM and tagged , , , . Bookmark the permalink.

Leave a Reply