Funktionale Elemente


 ... [ Groovy - Titelseite ] ... [ << Einfache Elemente ] ... [ Alles Dynamisch >> ] ...  




Übersicht:

  Closures
  Closures, Methoden und Multimethoden
  Currying
  Scope alias Gültigkeitsbereich
  Listen und Listenfunktionen
  Listen aufsplitten
  Wörterbücher (Maps)



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;




 ... [ Groovy - Titelseite ] ... [ << Einfache Elemente ] ... [ Alles Dynamisch >> ] ... [ nach oben ] ...