|
Übersicht:
Closures
Groovy enthält im Gegensatz zu Java und ähnlich
wie Python einige funktionale Sprachelemente. Dazu
gehören insbesondere die Closures und das
Currying. Closures werden von Groovy intensiv
unterstützt, während das Currying syntaktisch etwas
umständlich ist.
Eine Closure wird mit Hilfe der geschwungenen Klammern definiert:
{ println "hello" }
Closures können auch Parameter mitgegeben werden:
{ String s -> println s }
Closures können nun, wie alle anderen Objekte auch, in Variablen
gespeichert werden:
Closure c1 = { String s -> println s }
Durch diese Zeile wird noch nichts ausgegeben; nur die Variable c1
bekommt einen neuen Wert. Um c1 auszuführen gibt es zwei
Möglichkeiten:
c1.call("schall")
oder mit der speziellen Syntax für namenlose Aufrufe:
c1("rauch")
Alle Closures sind immer vom Typ Closure . Wird
eine Closure ohne Parameterangaben definiert, dann hat diese
automatisch einen Parameter namens it vom
Typ Object .
Rekursion erreicht man innerhalb von Closures mit dem
Schlüsselwort this :
Closure f = { n -> return n <= 1 ? 1 : n*this(n-1) }
assert f(2) == 2
assert f(3) == 6
assert f(4) == 24
Closures sind den Codeblöcken aus Java auf den ersten Blick sehr
ähnlich. Sie sind jedoch eher mit Lambda-Ausdrücken zu
vergleichen, da sie eine namenlose Funktion
repräsentieren. Closures "merken" sich zusätzlich zu dem
Funktionsrumpf auch Werte außerhalb der Funktion. Closures
können, wie alle Objekte, in Variablen gespeichert und bei
Funktionsaufrufen übergeben werden. Beispiele dazu folgen in den
nächsten Abschnitten.
Um Closures in Java zu implementieren, würde man anonyme,
innere Klasse nutzen, welche jedoch eine eher abschreckende
Syntax haben. Zudem würde man in Java viele Interfaces wie
zum Beispiel Runnable benötigen. Grundsätzlich
verhalten sich Closures wie anonyme, innere Klassen. Wenn man
in Groovy folgendes schreibt:
{ int i -> return i*2;}
Dann lautet der entsprechende Java-Code:
new Closure(){Object doCall(int i){return i*2;}}
Unterschiede gibt es allerdings beim
Gültigkeitsbereich.
Closures, Methoden und Multimethoden
Methoden und Closures sind sich sehr ähnlich. Daher gibt es
die Möglichkeit mit dem .& Operator eine
Methode in eine Closure umzuwandeln. Der Operator funktioniert
nur auf nicht statischen Methoden.
class Student
{
public String toString()
{
return "foo";
}
public String toString(String s)
{
return "foo" + s;
}
}
Student sven = new Student();
Closure c = sven.&toString;
assert c() == "foo";
assert c("blub") == "fooblub";
Der Operator
kann auch mit überladenen Methoden umgehen. Dabei wird das
später erläuterte
Verfahren
angewendet.
Currying
Als Currying bezeichnet man das Verfahren, welches eine Funktion mit
n Parametern und einem Parameter auf eine neue Funktion
mit n-1 Parametern abbildet. Die neue Funktion merkt sich
den Wert des ersten Parameters. Das Verfahren lässt sich wiederholen,
so dass man beliebig viele Parameter fixieren kann.
Closure s = { int a, int exp -> return a**exp; };
Closure s2 = s.curry(10);
assert s2(2) == 100;
In diesem Beispiel werden insgesamt zwei Closures erzeugt, s und
s2. Durch den Aufruf von s.curry(10) wird die zweite
Closure erzeugt, welche den Parameter 10 und eine Referenz auf s
speichert, und beim Aufruf im assert-Statement diesen Parameter an s
übergibt.
Als nächstes betrachten wir eine Closure mit 3 Parametern, um zu
demonstrieren, dass curry auch mit mehreren Parametern gleichzeitig
funktioniert:
Closure c = {
String hello, String name, int alter ->
return "$hello $name $alter";
};
Closure c1 = c.curry("Moin");
Closure c2 = c1.curry("Sven");
Closure c3 = c2.curry(42);
assert c1("Sven", 42) == "Moin Sven 42";
assert c2(42) == "Moin Sven 42";
assert c3() == "Moin Sven 42";
assert c.curry("Moin", "Sven")(42) == "Moin Sven 42";
assert c.curry("Moin", "Sven", 42)() == "Moin Sven 42";
In dem Beispiel werden
insgesamt vier Closures erzeugt (ohne assert-Statements). c1 merkt
sich beim Erzeugen mit Hilfe von c.curry("Moin") den
String-Parameter "Moin" und eine Referenz auf c. c2 merkt sich beim
Aufruf von c1.curry("Sven", 42) die beiden Parameter,
sowie eine Referenz auf c1.
Die Funktion des Currying (also curry ,
hier selfcurry genannt) ist relativ leicht
nachzubauen:
Closure c = {
String hello, String name, int alter ->
return "$hello $name $alter";
};
Closure selfcurry = {
Closure parent, Object[] params ->
return {
Object[] rest ->
return parent.call(*(params.toList()), *(rest.toList()));
}
};
Closure c1 = selfcurry(c, "Moin");
Closure c2 = selfcurry(c1, "Sven");
Closure c3 = selfcurry(c2, 42);
assert c1("Sven", 42) == "Moin Sven 42";
assert c2(42) == "Moin Sven 42";
assert c3() == "Moin Sven 42";
assert selfcurry(c, "Moin", "Sven")(42) == "Moin Sven 42";
assert selfcurry(c, "Moin", "Sven", 42)() == "Moin Sven 42";
assert selfcurry(selfcurry(c, "Moin", "Sven", 42))() == "Moin Sven 42";
toList ist eine Hilfsfunktion, um
ein Object[] in eine Liste umzuwandeln. Dies ist
nötig, da sich nur Listen mit
dem Stern-Operator
"zerpflücken" (engl. spread) lassen, Arrays jedoch nicht. Die
Arrays sind wiederum nötig, um variable Parameterlisten zu
implementieren.
selfcurry bekommt als Parameter eine Closure, sowie eine
beliebige Liste von Parametern, welche fixiert werden sollen. Der
Rückgabewert soll ausführbar sein, daher wird eine Closure
zurückgegeben. Diese Funktion erwartet eine beliebige Liste von
Parametern (rest), die sie zusammen mit den Parametern von selfcurry
an die übergebene Closure weiterleitet.
Scope alias Gültigkeitsbereich
Der Sichtbarkeitsbereich verhält sich bei Closures anderes als
bei inneren Klassen in Java. Dazu betrachten wir das folgende
Beispiel.
Groovy:
class Scope
{
int f()
{
return 0;
}
Closure makeFunction()
{
int i = 42;
Closure res = {
assert i == 9; //!
assert f() == 0; //!
};
i = 9;
return res;
}
public static void main(String[] args)
{
Scope s = new Scope();
Closure r = s.makeFunction();
r.call();
}
}
Java:
class Scope
{
int f()
{
return 0;
}
Runnable makeFunction()
{
final int i = 42;
Runnable res = new Runnable(){
public void run() {
assert i == 42; //!
assert f() == 0; //!
}
};
// i = 9;
return res;
}
public static void main(String[] args)
{
Scope s = new Scope();
Runnable r = s.makeFunction();
r.run();
}
}
Die Implementierung in Java muss die lokale
Variable i zwingend final
deklarieren. Diese Vorschrift entfällt in Groovy.
In Java wird zur Compilezeit entschieden, auf
welches f sich die Assertion bezieht. In Groovy
wird die Closure res zur Laufzeit
zunächst res.f() aufrufen. Da in der
Klasse Closure eine solche Funktion nicht
existiert, wird invokeMethod aufgerufen, welches
an den owner der Closure delegiert, hier also an
ein Scope Objekt. Auf diesem Objekt wird dann
schließlich f()
aufgerufen. Der owner ist immer das Objekt, in
dem die Closure konstruiert
wird. Im nächsten Kapitel
wird näher darauf eingegangen.
Listen und Listenfunktionen
Im Gegensatz zu Java bietet Groovy auf Syntax-Ebene
Unterstützung für die häufigsten Datentypen: List
und Map.
Ein einfaches Beispiel für die Benutzung von Listen:
List l1 = ["erdbeer", "vanille", "schoki"];
l1.each{ println it }
println l1.join(" & ");
Der each
Methode wird eine Closure übergeben, die jeweils ein einzelnes
Objekt mittels println ausgibt.
Die Methode each führt die übergebene Closure für
jedes Element der Liste einmal aus. it ist der
Default-Parameter einer jeden Closure, wenn kein anderer
Parameter deklariert wird. each ist
ungewöhnlicherweise nicht in Collection
deklariert, sondern in Object . Damit soll ein
with -Konstrukt erzeugt werden:
foo().bar().blub().getObjectWithVeryLongName().each{
println it;
bow(it);
someFuncWithLongName(it);
}
Die Implementierung von each in Object ruft die übergebene
Closure mit this als Parameter auf.
Die Funktionen head und tail für beliebige Listen, kann man
implementieren, indem man vom Index-Operator und Ranges
Gebrauch mach:
Closure head = {
List l -> l[0];
};
assert head([42, 43, 44]) == 42;
assert head([142..150]) == 142..150; //!!
Closure tail = {
List l ->
return l[1..-1];
};
assert tail([1, 3, 2]) == [3, 2];
assert tail([4, 6, 1, 3, 2]) == [6, 1, 3, 2];
Eine wichtige Funktion in der funktionalen Welt ist map
welche eine Liste l und eine Funktion f als Eingabe bekommt, und daraus
eine neue Liste (der gleichen Länge wie l) erstellt, indem auf jedes
Element einmal f angewendet wird.
Closure map = {
List l, Closure c
->
if(l == []) return [];
return [c(l[0])] + this(tail(l), c);
}
List input = [2, 3, 5, 7]
Closure c = { it -> it*2 }
assert map(input, c) == [4, 6, 10, 14]
Mit der Funktion filter lassen sich alle Elemente
einer Liste l herausfinden, welche ein bestimmtes
Prädikat p erfüllen. Die Funktion p
muss ein Element aus l auf einen booleschen Wert
abbilden. Die filter Funktion heisst in der
aktuellen Groovy-Versio grep , obwohl sie mit
Regulären Ausdrücken zunächst nichts zu tun hat.
Closure filter = {
List l, Closure p ->
if(l == []) return [];
return (p(l[0]) ? [l[0]] : []) + this(tail(l), p)
}
Closure even = { zahl -> zahl % 2 == 0 }
List input = 2..10
assert filter(input, even) == [2, 4, 6, 8, 10]
List longer = 1..100
assert filter(filter(longer, even), { it < 5}) == [2, 4]
Die Funktion reduce reduziert jeweils zwei Elemente einer
Liste anhand einer Vergleichs-Closure zu einem neuen Element. Dies
wird solange wiederholt, bis nur noch ein Element übrig bleibt.
Mit reduce lässt sich zum Beispiel das Minimum einer
Liste finden, oder auch die Summe.
reduce benötigt als Eingabe eine Liste, eine
zweistellige Funktion und ein Startobjekt und liefert ein
beliebiges Element.
Closure tail = {
List l ->
return l.size() <= 1 ? [] : l[1..-1];
};
assert tail([]) == [];
Closure reduce = {
Closure c, Object initElem, List l ->
assert c.call(initElem, initElem) == initElem;
if (l == []) return initElem;
return c.call(l[0], this.call(c, initElem, tail(l)));
};
Closure add = {a,b -> a+b};
Closure mul = {a,b -> a*b};
Closure sum = reduce.curry(add, 0);
Closure product = reduce.curry(mul, 1);
assert sum([]) == 0;
assert sum([42]) == 42;
assert sum([4, 5]) == 9;
assert product([3, 4, 5]) == 3*4*5;
assert product([13, 4, 5]) == 13*4*5;
Die drei Beispiele zeigen, dass in Groovy die Typisierung
von Closures sehr schwach ist, bzw ganz wegfällt. Man kann
nicht automatisiert fordern, daß eine (als Parameter
übergebene) Closure eine bestimmte Anzahl an Parametern
akzeptiert und somit auch keine Anforderungen an den Typ der
Parameter stellen. Auch ist es nicht möglich einen bestimmten
Rückgabewert zu fordern. Dies ist der größte Nachteil von
Closures gegenüber anonymen Klassen in Java. Es gibt in der
Klasse Closure zwar Methoden, die die Funktion näher
beschreiben und somit auch Tests zulassen; eine Unterstützung
auf Syntax-Ebene existiert jedoch nicht.
Listen aufsplitten
Der * Operator (engl. spread operator)
zerlegt eine Liste in ihre Einzelteile. Er ist somit die
inverse Funktion zum [] Operator, der aus
Elementen eine Liste erzeugt. In dem folgenden Beispiel
benötigt die Funktion add zwei Parameter, welche
allerdings in einer Liste zu Verfügung stehen.
int add(int a, int b)
{
return a+b;
}
List ints = [40, 2];
assert add(*ints) == 42;
Die
Verwendung von *ints ist somit äquivalent zur
(theoretischen) Schreibweise ints[0], ints[1], ...,
ints[ints.size()-1] . Es lassen sich nur Listen
aufsplitten, Arrays müssen erst in eine Liste konvertiert
werden.
Wörterbücher (Maps)
Eine Map ist eine Liste von Schlüssel-Wert-Paaren, wobei die
Schlüssel in der gesamten Map eindeutig sind, die Werte aber
beliebig oft vorkommen können. Eine Map ist somit die
Verallgemeinerung eines Arrays, bei dem die Schlüssel immer
(positive) ganze Zahlen sein müssen. Groovy bietet eine
spezielle Syntax für die einfache Eingabe von Maps:
Map empty = [:]
Map eispreise = [
"vanille": 0.50,
"erdbeer": 0.60,
"schoki": 0.70,
]
eispreise.each{ k, v -> println "Es gibt $k für $v"}
eispreise.each{ it -> println "Es gibt $it.key für $it.value"}
Die
Methode Map.each(Closure c) kann als Parameter
eine Closure mit einem oder zwei Parametern erhalten. Um dies
zu ermöglichen, muss each prüfen, wie viele
Parameter die Closure hat, und dementsprechend eine Variante
auswählen. Dies ist mit der
Methode getParameterTypes().size() möglich.
Maps werden
für benannte
Parameter benötigt.
Es gibt verschiedenen Möglichkeiten, auf die Elemente einer
Map zuzugreifen, ohne explizit eine Methode aufzurufen
(Implizit wird natürlich immer eine Methode aufgerufen). Der
erste Weg ist das Nutzen des Index-Operators:
Map m = [a:"schall", b:"und", c:"rauch"];
assert m['b'] == "und";
assert m['foo'] == null;
Bei der
zweiten Möglichkeit wird automatisch die
Methode getProperty aufgerufen, welche
später
erläutert wird.
Map m = [a:"schall", b:"und", c:"rauch"];
assert m.b == "und";
assert m."a" == "schall";
assert m.foo == null;
|