Die Aufgabe problemspezifischer Sprachen (Domain Specific Languages, DSL) ist es, möglichst intuitiv eine recht konkrete Aufgabe zu lösen. Dazu verwenden sie ein möglichst einfaches Vokabular, das zum Problemfeld passt und den Benutzer nicht überfordert. Solche kleinen Spezialsprachen haben in Unix eine lange Tradition, wie der Kasten "DSLs in der Praxis" beweist. Eine moderne Skriptsprache wie Ruby macht durch ihre flexible Syntax das Schreiben von Domain Specific Languages einfach.
Extern und intern
Eine DSL heißt extern, wenn ihre Verarbeitung die Umwandlung in eine andere Sprache oder einen speziellen Interpreter voraussetzt. Für externe DSLs kann der Entwickler eine eigene Syntax definieren, ohne sich den Regeln einer existierenden Sprache zu unterwerfen. Eine interne DSL bedient sich im Gegensatz dazu direkt der Ausdrucksmittel einer Programmiersprache.
Interne DSLs brauchen keinen eigenen Parser, die syntaktische Richtigkeit des Code überprüft der Compiler oder Interpreter. Die Entwicklungsumgebungen kennen die Sprachsyntax, rücken also beispielsweise korrekt ein und färben Sprachkonstrukte farbig.
Ruby eignet sich wegen ihrer vielfältigen Metaprogramming-Fähigkeiten, ihrer offenen Klassen und des flexiblen Objektmodells sehr gut zum Schreiben interner DSLs. Blocks erleichtern es beispielsweise, diese um eigene Sprachelemente zu erweitern und Anweisungen in einem festgelegten Kontext auszuführen. Auch die Möglichkeit, Syntaxelemente abzukürzen oder wegzulassen, erleichtert die Formulierung sauberer DSLs. Die folgenden Abschnitte stellen anhand einiger Beispiele die wichtigsten Sprachelemente für DSLs in Ruby vor.
Die Programmiersprache bietet diverse Möglichkeiten, um die syntaktische Komplexität zu minimieren. Dazu ein Beispiel im Stil der DSL von Switchtower, einem auf die Verteilung von Webanwendungen mit Ruby on Rails [1] spezialisierten Deployment-Tool. Die Switchtower-DSL lässt den Anwender Aufgaben definieren, die auf verschiedenen Servern laufen sollen:
task("backup", { "roles" => "db" })
Die Aufgabe bekommt mit dem ersten Parameter der Methode »task« den Namen »backup« zugeteilt. Manche Aufgaben sollen auf jedem Server ausgeführt werden, andere nur auf einzelnen. Die Parameter in Form eines Hash legen fest, dass die Aufgabe »backup« nur auf Servern mit der Rolle »db« laufen soll.
Besser: Weniger Klammern
In Ruby sind runde Klammern um die Variablen eines Methodenaufrufs nur zu setzen, wenn der Code sonst für den Interpreter nicht eindeutig wäre. So lässt sich die obige Aufgabe umformulieren: »task "backup", { "roles" => "db" }«. Ruby erkennt Hashes am Ende der Parameterliste auch ohne die sonst nötigen geschweiften Klammern. Das vereinfacht den Funktionsaufruf nochmals zu »task "backup", "roles" => "db"«.
Kommt in einem Ruby-Programm eine fixe Zeichenkette vor, empfiehlt es sich meist, sie durch ein Symbol zu ersetzen. Kommt das gleiche Symbol im Programm mehrfach vor, belegt es nur einmal Speicher und ist einfacher zu schreiben als eine normale Zeichenkette.
Statt der Anführungszeichen am Anfang und Ende des Strings ist nur ein führender Doppelpunkt notwendig. Das obige Beispiel reduziert sich damit auf »task :backup, :roles => :db«. Natürlich sind diese Änderungen kosmetischer Natur. Bei DSLs ist aber einfache Handhabung besonders wichtig, wozu auch ein klares Erscheinungsbild gehört.
Zu den wichtigsten Elementen von Ruby gehören Blocks, abgeschlossene Codebereiche, die eine Methode bei ihrem Aufruf übergeben bekommt. Im Normalfall folgt der Block nach der schließenden runden Klammer. Anfang und Ende des Blocks markieren entweder »do« und »end« oder geschweifte Klammern. Der Block läuft immer in dem Kontext ab, in dem er definiert wurde. In der Methode ruft die Anweisung »yield« den Block auf. Die Variablenzuweisung im Block findet zwischen Pipezeichen statt.
|
Populäre Beispiele für Domain Specific Languages sind Regular Expressions, die SVG-Definition für Vektorgrafiken, der Pattern-Scanner Awk oder Konfigurationssprachen wie bei Sendmail. Allen ist gemeinsam, dass sie für einen speziellen Einsatzzweck optimiert sind. Im Unix-Bereich sind seit langem die beiden Tools Lex und Yacc auf das Erstellen und die Verarbeitung von Domain Specific Languages spezialisiert.
In der Ruby-Welt gibt es viele Beispiele für interne DSLs: Das Make-ähnliche Build-Tool Rake [2] verwendet eine DSL für die Konfiguration abhängiger Aufgaben. Das ORM-Paket Active Record [3] benutzt eine sehr einfache DSL zur Beschreibung der Beziehungen zwischen Datenbanktabellen.
Das Deployment-Werkzeug Switchtower [4] wiederum setzt eine DSL dazu ein, um notwendige Aktionen für die Verteilung einer Webanwendung zu definieren. Builder [5] schließlich ist ein Werkzeug für die Erzeugung von XML-Files und ein Stellvertreter für besonders dynamische DSLs: Das Tool fängt alle Aufruf einer Methode ab und wandelt sie in entsprechende XML-Tags um.
|
Blocks der Art »do |var| puts var end« lassen sich in einer DSL sehr gut verwenden, um in beschreibender Form Einstellungen vorzunehmen:
task :backup, :roles => :db do
run "backup_command"
end
Eines der mächtigsten Features von Ruby sind die offenen Klassen. Damit ist es jederzeit möglich, Klassen und deren Methoden auch nach ihrer ursprünglichen Definition zu verändern. Das gilt sogar für die Ruby-Basisklassen. Ein gutes Beispiel für eine entsprechende Veränderung der Basisklassen findet sich im Active-Support-Modul [6]. Es überlädt unter anderem die Numeric-Klasse, um den Umgang mit Zeitangaben intuitiver zu gestalten:
class Numeric
def minutes
self * 60
end
def hours
minutes * 60
end
end
Als Beispiel wäre eine Anweisung der Form »5.minutes + 1.hours« denkbar. Eine kleine Änderung erhöht die Lesbarkeit der DSL noch einmal. Während »5.minutes« bereits sehr gut lesbar ist, trifft das auf »1.hours« nicht zu. Stattdessen wäre es passender, »1.hour« schreiben zu können. Da die bestehende Version programmtechnisch richtig funktioniert, fehlt nur eine Kopie der Methode. Das ist leicht mit der Ruby-Methode »alias« zu erreichen:
class Numeric
def hours
minutes * 60
end
alias :hour :hours
end