Eine skalierbare Sprache für die JVM

Scala verändert die Java-Welt

Scala ist eine ziemlich coole Sprache für die Java Virtual Machine, der neue Stern am Java-Himmel. Grund genug, sie in einer vierteiligen Artikelserie vorzustellen. Im ersten Teil geht es um die Grundzüge einer Sprache, die ausdrucksstark und dennoch statisch getypt ist.

Ein Artikel kann natürlich nicht die komplette Sprache erklären, ja eigentlich nicht einmal ein Grundlagen-Tutorial sein – dafür ist hier einfach nicht genug Platz. Aber ich kann zeigen, was mir an Scala gefällt, und die Grundkonzepte vorstellen. Detaillierte Informationen gibt es auf der Scala-Homepage, insbesondere in der Sprachspezifikation. Für die Vertiefung sei das exzellente Buch„Programming in Scala“ empfohlen.

Überblick

Scala hat eine Menge mit Java gemeinsam, was nicht weiter verwundert – schließlich läuft es auf der Java VM. Es hat dieselben primitiven Typen, und Dinge wie Annotationen, Exceptions oder Generics sind fast identisch. Der Hauptunterschied zu Java ist, dass Scala eine erweiterbare, skalierbare Sprache ist. In Java gibt es für viele Problemstellungen Bibliotheken – das funktioniert, ist aber in der Syntax oft etwas umständlich. Bei Skriptsprachen dagegen sind viele Features wie reguläre Ausdrücke oder Collection-Literale Teil des Sprachkerns und haben eine entsprechend einfache Syntax. So nützlich solch syntaktischer Zucker bei Skriptsprachen ist, erstreckt er sich jeweils nur auf die begrenzte Menge an Features, die direkter Bestandteil der Sprache sind. Aber was ist, wenn man Vektorarithmetik haben will? Oder Aktor-basierte Parallelverarbeitung?

Scala geht einen dritten Weg. Es hat einen relativ schlanken Sprachkern, bietet aber die Möglichkeit, Bibliotheken zu schreiben, die sich nahtlos in die Sprache einfügen und die kaum von eingebauten Sprachfeatures zu unterscheiden sind. Scala ist eine funktionale Sprache, d. h. Funktionen sind vollwertige Sprachelemente. Es gibt natürlich auch Closures als Sonderfall von Funktionen und man kann Funktionen teilweise evaluieren. Damit man möglichst viele Vorteile aus der funktionalen Programmierung ziehen kann, unterstützt Scala seiteneffektfreies Programmieren. Neben Variablen gibt es Konstanten, Pattern Matching, seiteneffektfreie Collection-Klassen, und Scala optimiert rekursive Funktionen (Tail Recursion). Die Sprache erzwingt aber nicht einen „reinen“ funktionalen Stil, sondern bietet ihn pragmatisch als eine Option von mehreren an. Außerdem ist Scala konsequent objektorientiert. Auch Zahlen sind Objekte, und die Unterscheidung zwischen int und java.lang.Integer entfällt. Es gibt auch kein static, weder für Felder noch für Methoden, stattdessen kann man neben Klassen auch benannte Objekte definieren. Darüber hinaus bietet Scala eine Vielzahl von ausdrucksstarken Sprachmitteln, mit denen man Bibliotheken schreiben kann, die sich wie eingebaute Sprachfeatures anfühlen. Und auf Basis dieser Features bringt Scala eine Reihe extrem eleganter und nützlicher Bibliotheken mit, z.B. eine Concurrency-Bibliothek auf Basis von Aktoren im Stile von Erlang.

Hallo Welt!

Doch genug der Vorrede – die Ärmel hochgekrempelt und auf zu einem ersten Programm. Man kann Scala auf der Kommandozeile kompilieren oder starten, aber mit einer IDE macht das Arbeiten besonders am Anfang natürlich mehr Spaß. Die Scala-Distribution enthält einen Interpreter, mit dem man interaktiv einzelne Befehle ausprobieren kann. Es gibt aber auch Plug-ins für die gängigen IDEs. Der erste Schritt besteht im Anlegen eines Scala-Projekts (Eclipse-Terminologie). In diesem Projekt legen wir die Datei HelloWorld.scala an, deren Endung .scala sie als Scala-Quelltext auszeichnet. In dieser Datei landet der Quelltext, der unser erstes Scala-Programm darstellt. Wenn wir dieses Programm starten (STRG+F11 in Eclipse), dann gibt es erwartungsgemäß Grüße aus:

object HelloWorld extends Application { println ("Hallo Arno...") println ("... und hallo Welt") } 

Scala kennt neben Klassen auch Objekte als direkte Sprachkonstrukte – Singletons, die unter ihrem Namen überall verfügbar sind und mit dem Schlüsselwort object definiert werden. Sie haben in etwa die Rolle, die static in Java hat. Hier interessiert uns zunächst nur, dass ein Objekt, das von Application erbt, direkt Scala-Code enthalten kann, den man dann als Programm starten kann. Semikolons sind in Scala optional, zumindest am Ende einer Zeile.

Variablen

Scala kennt zwei Arten von Variablen, die mit den Schlüsselworten val und var definiert werden. Ein var (wie „variable“, also veränderlich) ist dabei eine „normale“ Variable, der man beliebig oft neue Werte zuweisen kann. Ein val (wie „value“, also Wert) entspricht dagegen in etwa einer final-Variable in Java: Dieser erhält zu Beginn einen Wert, den man nie mehr ändern kann. Spätere Zuweisungen sind verboten. val ist der Normalfall in Scala, und es passt besonders gut zu einem funktionalen Programmierstil. Folgendes Beispiel zeigt Auszüge für beide Arten von Variablen. Dabei habe ich wie auch im Folgenden den „Rahmen“ eines Objekts, das von Application erbt, weggelassen:

val i = 99 val j: Int = 3*i // optionale Typ-Deklaration var vorname = "Arno" val nachname = "Haase" vorname = "Markus" // funktioniert, weil var nachname = "Mustermann" // compile-Fehler! 

Die Deklaration des Typs einer Variablen ist dabei optional. Wenn man keinen Typ angibt, nimmt Scala den Typ des Wertes, mit dem die Variable initialisiert wird (Type Inference). Das vermeidet überflüssige Angaben im Quelltext und macht sie deutlich lesbarer – man denke nur an die Initialisierung einer Map mit Generics in Java, wo man die Typdefinition fast identisch doppelt hinschreiben muss. Scala ist aber – im Gegensatz z.B. zu Groovy – stark typisiert. Auch Variablen ohne explizit angegebenen Typ haben einen Typ, genau so, als ob man ihn hingeschrieben hätte. Und wenn man mit der Variable Dinge tut, die nicht zu ihrem Typ passen, meldet der Compiler einen Fehler, genau wie man es von Java her kennt.

[ header = Seite 2: Funktionen ]

Funktionen

Funktionsdefinitionen beginnen in Scala mit dem Schlüsselwort def (Listing 1).

// ausführliche Syntax def abs1 (i: Int): Int = { if (i >= 0) i else -i } // kompakte Syntax def abs2 (i: Int) = if (i >= 0) i else -i println (abs1 (-1)) println (abs2 (-2)) // ohne Parameter def hallo = println ("Hallo") hallo // mehrere Expressions im Body def a = { println ("Anfang von a") 9 8 7 } println (a) 

Nach dem Funktionsnamen kommt die Parameterliste, nach einem Doppelpunkt der Rückgabetyp und schließlich nach einem Gleichheitszeichen die Implementierung. Der Rückgabetyp einer Funktion ist genau wie bei einer Variablendefinition optional, zumindest, wenn die Funktion nicht rekursiv ist. Wenn man ihn weglässt, bestimmt Scala ihn per Type Inference aus der Implementierung der Funktion. Auch die geschweiften Klammern um den Body der Funktion kann man weglassen, wenn sie nur aus einer einzigen Expression besteht.

Aber Augenblick mal, wieso Expression – in der Funktion steht doch ein if? In Scala ist alles eine Expression, alles hat einen Rückgabetyp und liefert einen Wert zurück (für void und andere Sonderfälle siehe hier). Ein if-else ist in Scala jedenfalls im Gegensatz zu Java eine vollwertige Expression, die eben entweder den Wert des if-Zweigs oder den Wert des else-Zweigs zurückliefert. Es entspricht dem „? :“-Operator in Java, aber mit einer sprechenderen Syntax. Wenn eine Funktion keine Parameter hat, kann man die leere Parameterliste in der Deklaration und auch beim Aufruf weglassen. Und wenn mehrere Ausdrücke hintereinander stehen, führt Scala sie alle aus und liefert den letzten Wert zurück. Auf diese Weise kann man sich in den meisten Fällen ein explizites return sparen.

Schleifen

Neben der Fallunterscheidung mit if-else sind Schleifen die zweite wichtige Kontrollstruktur, beispielsweise:

val namen = List("Arno", "Sven", "Kirstin") for (n <- namen) println (" " + n) namen.foreach (n => println ("foreach " + n)) for (i <- 1 to 5) println (i) 

Eine for-Schleife in Scala iteriert immer über den Inhalt einer Collection. Deshalb ist der erste Schritt, eine Liste mit Namen anzulegen, über die wir dann iterieren können. Bei der ersten Schleife steht n <- namen in den Klammern, wobei man den Pfeil nach links als in lesen kann. Jedes der Elemente der Liste wird der Reihe nach an den val n gebunden und dann jeweils der Inhalt der Schleife ausgeführt. Alternativ kann man auf der Liste die Methode foreach aufrufen und den Code als Parameter übergeben, der für jedes Element ausgeführt werden soll. Dies ist das erste Mal, wo wir funktionaler Programmierung in Scala begegnen: Der einzige Parameter von foreach ist eine Funktion. Diese Funktion ist eine so genannte Closure, eine anonyme Funktion, die man direkt im Code hinschreibt. Sie hat einen Parameter, nämlich n, und sie gibt einen übergebenen Wert mittels println aus. Die Deklaration des Parameters ist von der Implementierung der Funktion durch => getrennt. Die dritte Schleife schließlich geht die Zahlen von 1 bis 5 durch und gibt sie aus. Der Ausdruck 1 to 5 sieht dabei auf den ersten Blick wie eingebaute Syntax für den Fall aus, dass man eine Schleife über einen Zahlenbereich laufen lassen will. In Wirklichkeit ist to aber eine Methode, die für den Typ Int definiert ist und die einen Iterator zurückliefert. Auf diese Weise hat man eine intuitive Syntax, die wie ein Bestandteil der Sprache aussieht, in Wirklichkeit aber als Bibliothek implementiert ist.

Der Methodenaufruf ist auf den ersten Blick nicht von einem Schlüsselwort der Sprache zu unterscheiden, weil man beim Aufruf einer Methode mit genau einem Parameter den Punkt und die Klammern weglassen kann. Der Ausdruck 1 to 5 ist also mit dem Ausdruck 1.to (5) identisch – allerdings deutlich besser lesbar. Mit dieser optionalen Infix-Notation kann man – besonders im Zusammenspiel mit Closures – Bibliotheken schreiben, die sich auf natürliche Weise in die Sprache einfügen und sie scheinbar erweitern. Wir werden im Laufe der Serie immer wieder Beispiele dafür sehen.

Collection-Klassen

Wie es für funktionale Programmiersprachen üblich ist, bringt auch Scala eine umfangreiche und leistungsfähige Bibliothek an Collection-Klassen mit. Listings 2a-d zeigen eine kleine Auswahl daraus.

val städte = List ("Braunschweig", "Wolfsburg", "Pusemuckel") println (städte (0)) // Kopie statt Modifikation val mehrStädte = städte + "Flensburg" println (städte.contains ("Flensburg")) println (mehrStädte) 

Listen sind die wahrscheinlich gebräuchlichste Art von Collections. Man kann sie in Scala direkt durch Hinschreiben der Werte definieren – man schreibt sie als kommaseparierte Liste in Klammern hinter „List“. Diese Kurzschreibweise erinnert etwas an die eingebaute Syntax für Listen, die viele Skriptsprachen haben. Bei Scala handelt es sich aber nicht um ein eingebautes Sprachfeature, sondern es ist eine Factory-Methode aus der Bibliothek. Die Liste, die man auf diese Weise bekommt, ist unveränderlich (immutable). Man kann auf ihre Werte zugreifen, aber man kann weder Einträge hinzufügen noch entfernen, und man kann einen bestehenden Eintrag nicht ändern. Wenn man z.B. mit dem +-Operator einen Eintrag anhängt, wird die bestehende Liste nicht verändert, sondern es wird eine neue Liste erzeugt, die das zusätzliche Element enthält. Diese Art von Listen ist typisch für funktionale Programmiersprachen. Sie ist für Java-Entwickler vielleicht etwas ungewohnt, aber sie hilft dabei, überraschendes Programmverhalten zu vermeiden. So ist sie z.B. automatisch Thread-sicher, und es kann auch nicht passieren, dass ein völlig anderer Teil des Programms eine Liste verändert und man sich wundert, wo die Daten geblieben sind. Listen sind in Scala einfach verkettet, daher sind sie für manche Algorithmen sehr gut geeignet, bei denen aber wahlfreier Zugriff recht teuer ist. Eine Alternative dazu sind Arrays (Listing 2b).

[ header = Seite 3: Fazit ]

val länder = Array ("Sachsen", "Thüringen") println (länder (0)) // Arrays sind veränderlich länder (0) = "Sachsen-Anhalt" println (länder (0)) // Anhängen nur als Kopie val mehrLänder = länder + "Bayern" println (länder.contains ("Bayern")) 

Arrays werden unter der Oberfläche auf Java-Arrays abgebildet und sind entsprechend effizient – sie sind aber in Scala vollwertige, normale Objekte. So gibt es auch eine Factory-Methode für das Erzeugen von Arrays, die analog zu derjenigen für Listen ist. Man kann einzelnen Einträgen in einem Array neue Werte zuweisen, Arrays sind also modifizierbar. Sie können aber nicht wachsen oder schrumpfen – das ist eine Beschränkung der JVM. Die Methoden, die Werte hinzufügen oder entfernen, erzeugen deshalb eine modifizierte Kopie des Arrays und liefern sie zurück.

val tupel = ("a", 1, 2.34) // Tupel-Elemente sind stark getypt def x (s: String) = println (s) x (tupel._1) 

Ein weiterer nützlicher Datentyp sind die Tupel (Listing 2c). Ein Tupel ist so etwas wie eine feste Liste von Daten. Der Unterschied zu einer Liste liegt darin, dass Tupel nicht zum Hinzufügen oder Entfernen von Objekten gedacht sind, sondern eben dazu, eine feste Gruppe von Werten festzuhalten. Außerdem hat ein Tupel für jedes Element einen eigenen Typ, der auch dem Compiler zur Verfügung steht. Tupel sind ein Datentyp, der zum Sprachkern von Scala gehört, und dem entsprechend ist die Syntax für Tupel im Quelltext besonders einfach: Man schreibt die Werte einfach kommasepariert in Klammern. Die Werte eines Tupels liegen in vals, die als Namen einen Unterstrich und danach die laufende Nummer haben, also _1, _2, _3 usw. Ja, die Zählung beginnt tatsächlich bei 1. Und weil die Werte in vals liegen, ist ein Tupel unveränderlich. Tupel dienen u.a. intern als Bausteine bei der Implementierung von anderen Collection-Klassen. Im Anwendungscode sind sie eher selten, können aber nützlich sein, um z.B. zwei Werte aus einer Funktion zurückzugeben.

val hauptstädte = Map ( "Niedersachsen" -> "Hannover", "Bayern" -> "München" ) println (hauptstädte ("Niedersachsen")) // Kopie statt Modifikation val mehrHauptstädte = hauptstädte + ("NRW" -> "Düsseldorf") println (hauptstädte.contains ("NRW")) println (mehrHauptstädte ("NRW")) 

Auch für Maps gibt es eine Factory-Methode (Listing 2d). Der Pfeil -> ist dabei kein eingebautes Sprachkonstrukt von Scala, sondern eine Methode mit einem Parameter, die man auf jedem Objekt aufrufen kann und die ein 2-Tupel mit den beiden Objekten zurückliefert. Scala erlaubt fast beliebige Sonderzeichen in Methodennamen, sodass -> eine völlig normale Methode ist. Zusammen mit der oben beschriebenen Inifix-Notation – beim Aufrufen einer Methode mit einem Parameter kann man den Punkt und die Klammern weglassen – erhält man die Pfeil-Schreibweise. Sie sieht so aus wie die eingebaute Syntax mancher Skriptsprachen, ist in Scala aber in einer Bibliothek definiert. Das ist ein weiteres Beispiel dafür, wie Scala „skalierbar“ ist, also die Möglichkeit bietet, zusätzliche Features als Bibliothek ziemlich nahtlos in die Sprache einzufügen. Die Map, die man im einfachsten Fall erhält, ist unveränderlich. Es gibt aber auch eine andere HashMap-Implementierung, die man genau wie in Java modifizieren kann. Das ist typisch für die Philosophie von Scala: Der funktionale, seiteneffektfreie Programmierstil ist am einfachsten und wird besonders gut unterstützt. Er wird aber nicht erzwungen, und man kann dort auf einen prozeduralen Stil wechseln, wo man das will.

… und ihre Methoden

Diese Collection-Klassen entfalten ihre besondere Stärke dadurch, dass man mithilfe von Closures in typisch funktionaler Weise mit ihnen arbeiten kann. Listing3 zeigt exemplarisch einige wichtige Methoden von Listen.

val numbers = List (1, 2, 5, 6, 9) numbers.foreach (i => println (" foreach: " + i)) println (numbers.count (i => i < 3)) println ("all postive: "+numbers.forall (i => i>0)) println ("has zero: "+numbers.exists (i => i == 0)) println ("even: " + numbers.filter (i => i%2 == 0)) println ("doubled: " + numbers.map (i => 2*i)) 

Die Methode foreach ist uns oben schon begegnet. Sie wendet die übergebene Funktion auf alle Elemente der Liste an. Count zählt die Elemente, die einer bestimmten Bedingung genügen, und zwar ohne, dass man die Iteration selbst programmieren müsste. ForAll und exists überprüfen, ob alle bzw. mindestens ein Element der Liste einer bestimmten Bedingung genügen. Solche Überprüfungen kommen erstaunlich oft vor, und wenn man sich erst einmal an die funktionale Lösung gewöhnt hat, merkt man, wie umständlich sie in Java-Code sind. Filter bietet die einfache Möglichkeit, nur solche Elemente zu bekommen, die eine Bedingung erfüllen – sei es wie hier, dass sie gerade Zahlen sind, sei es z.B. dass sie Geschäftsobjekte sind, die ihre Constraints verletzen. Und map schließlich wendet eine Funktion der Reihe nach auf alle Elemente an und liefert eine neue Liste mit den Ergebnissen. Im Beispiel von Listing 3 wird jede Zahl verdoppelt, man könnte auf diese Weise aber genauso gut jeweils zu einer Entität das entsprechende DTO erzeugen. Allen diesen Methoden ist gemeinsam, dass sie die Liste nicht verändern, sondern ihre Ergebnisse in einer separaten Liste zurückliefern. Dies ist nur eine kleine Auswahl der Collection-Methoden, die Scala mitbringt, und sie ist keineswegs spezifisch für Scala – im Gegenteil, viele funktionale Sprachen haben diese oder ähnliche Operationen. Die Auswahl zeigt aber, wie man mit Closures und überhaupt mit funktionaler Programmierung auf sehr kompakte, gut lesbare Art und Weise mit Collections hantieren kann.

Fazit

Scala ist eine coole Sprache, die reif und stabil ist – und in der man nicht nur kompakter, klarer und ausdrucksstärker programmieren kann als mit Java, sondern in der es außerdem noch besonders viel Spaß macht. Es gibt eine ziemlich gute Integration in alle gängigen Java-IDEs und die Sprache hat eine lebendige Community. Scala ist definitiv geeignet, um damit komplette Systeme zu bauen, man kann es aber auch gut mit Java-Code integrieren. Dieser erste Teil der Serie hat einen grundlegenden Überblick über die Sprache gegeben und hoffentlich den Appetit angeregt. Im zweiten Teil geht es dann an Objekte, Klassen und Vererbung in Scala.

Arno Haase ist selbständiger Softwarearchitekt aus Braunschweig. Er ist eines der Gründungsmitglieder von se-radio.net, dem Software Engineering Podcast. Außerdem spricht er regelmäßig auf Konferenzen und ist Autor diverser Artikel und Patterns sowie Coautor von Büchern über JPA und Modellgetriebene Softwareentwicklung. Kontakt: Arno.Haase@Haase-Consulting.com.
Geschrieben von
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.