Dienstag, 8. März 2011

Interne DSL mit Groovy Teil 1

Die zweite Sprache, die ich auf Eignung für interne DSLs testen will, ist Groovy. Groovy ist eine dynamische Sprache. Sie läuft ebenfalls in der Java VM. Die Syntax ist sehr ähnlich zu Java und damit wesentlich umfangreicher als die Syntax von Clojure. Dieses mehr an Syntax bedeutet gleichzeitig, dass mehr Techniken nötig sind um die Hostsprache Groovy so zu verbiegen, dass sie das "Look&Feel" einer DSL hat. In diesem ersten Teil werde ich einige Techniken von Groovy vorstellen, die zur Erstellung von internen DSLs genutzt werden können. Im zweiten Teil werde ich dann die Fragebogen DSL aus dem Clojure Beitrag mit Groovy umsetzen.

Techniken für die Erstellung von DSLs

Einsparung von Semikolons und Klammern

In Groovy sind Semikolons in den meisten Fällen optional. Klammern bei Funktionsaufrufen können auf der obersten Ebene ebenfalls entfallen.

//In Java und Groovy gültig
println("Test");
println("Hello"); println("World");
println(Integer.parseInt("3"));

println "Test"                                  //funktioniert
println "Hello" println "World"    //funktioniert nicht
println "Hello"; println "World"  //funktioniert
println Integer.parseInt "3"      //funktioniert nicht
println Integer.parseInt("3")     //funktioniert


Neue Methoden und Eigenschaften für Klassen zur Laufzeit hinzufügen

In Groovy ist es möglich jeder existierenden Klasse neue Methoden und Eigenschaften zur Laufzeit hinzuzufügen, ohne die Klassen neu kompilieren zu müssen. Dazu wird der ExpandoMetaClass-Mechanismus verwendet. Damit besteht z.B. die Möglichkeit Einheiten an Zahlen anhängen zu können (z.B. "1.km" oder "1.s") und entsprechende Umwandlungen durchzuführen, wie im folgenden Beispiel zu sehen ist.

//Vererbung von neuen Methoden und Eigenschaften welche über
//den ExpandoMetaClass-Mechanismus hinzugefügt werden ist
//standardmäßig deaktiviert. Mit der folgenden Zeile wird
//Vererbung aktiviert.
ExpandoMetaClass.enableGlobally()

Number.metaClass.getProperty = {String einheit ->
    switch(einheit) {
        //delegate ist die Instanz für welche die Eigenschaft
        //abgerufen wird.
        case 'km': return delegate * 1000
        case 'm': return delegate
        case 'cm': return delegate * 0.01
        case 'mm': return delegate * 0.001
        default: 'Die Einheit ' + einheit + ' ist unbekannt.'
    }
}

println(1.km)
println(10.cm)
println(20.ha)

Bei Ausführung des Skriptes entsteht folgende Ausgabe:

1000
0.10
Die Einheit ha ist unbekannt.

Mehr zu dem Thema kann man hier nachlesen.

Abfangen nicht definierter Variablen

In jedem Groovy-Skript existiert eine Instanz der Klasse "groovy.lang.Binding". Diese Instanz löst alle nicht im Skript definierten Variablen auf. Dieser Mechanismus wird u.a. genutzt um Informationen zwischen dem Groovy Skript und der aufrufenden Sprache auszutauschen. Die aktuelle Instanz ist über die Variable "binding" erreichbar. Ersetzt man diese aktuelle Instanz mit einer Instanz einer eigenen Klasse welche von "groovy.lang.Binding" erbt, kann man das Verhalten des Skriptes bei nicht definierten Variablen selber festlegen. Das folgende Beispiel demonstriert diese Möglichkeit.

class TestBinding extends Binding {
    def getVariable(String name) {
      println("Variable nicht gefunden")
      return null
    }
}

binding = new TestBinding()

nichtDefinierteVariable

Die Ausgabe bei Ausführung des Skriptes ist "Variable nicht gefunden", der Rückgabewert ist null.

Überladen von Operatoren

Ähnlich wie in C++ können Operatoren in Groovy überladen werden. Jeder Operator wird durch eine Methode repräsentiert, welche man in der eigenen Klasse einfach implementieren muss, um den entsprechenden Operator zu überschreiben. Eine Liste mit den Methodennamen findet man hier.

class Kaffe {
    List zutaten = []
    
    def leftShift(String zutat) {
        this.zutaten << zutat
    }
    
    def inhalt() {
        println("Im Kaffe ist: ")
        zutaten.each {it ->  print("* "); println(it)}
    }
}

kaffe = new Kaffe()
kaffe << "Wasser" 
kaffe << "gemahlene Bohnen" 
kaffe << "Milch"
kaffe.inhalt();

Ergibt die Ausgabe

Im Kaffe ist: 
* Wasser
* gemahlene Bohnen
* Milch

Methodenzeiger

Es ist möglich Zeiger auf Methoden einer bestimmten Instanz einer Klasse zu erstellen.

class Pflanze {
    def int wassermenge
    def gießen(int menge) {
        wassermenge += menge
    }
}

kaktus = new Pflanze();
def gieße = kaktus.&gießen

gieße 300
gieße 100

Links

Keine Kommentare:

Kommentar veröffentlichen