Domänenspezifische Sprachen und Ruby

Peter Schrammel, 2. März 2009 14:00

peter_dslDomänenspezifische Sprachen, domain specific languages (DSLs) sind in der IT- und Ruby-Welt sehr verbreitet und werden (teilweise unbewusst) täglich benutzt. Man denke an Make, Rake, acts_as_state_machine und rSpec. An einem kleinen Beispiel soll gezeigt werden wie man selbst einfach DSLs bauen kann.

Mit DSLs will man folgende Ziele erreichen:

  • weniger Redundanz
  • deklarative Beschreibung eines Sachverhaltes
  • bessere Lesbarkeit
  • weniger technischer Code
  • leichte Erlernbarkeit aufgrund des beschränkten Umfangs

Die DSLs lassen sich grob in zwei Klassen unterteilen:

Externe DSLs werden durch einen, meist speziell dafür geschriebenen, Parser eingelesen und dann interpretiert.

Vorteile:

  1. Die DSL kann vollkommen frei aufgebaut werden.
  2. Ein Dokument der DSL wird Interpretiert (gelangt also nicht direkt zur Ausführung) wodurch mögliche Sicherheitslücken vermieden werden.

Nachteile:

  1. Für jede DSL muss ein Parser geschrieben werden.
  2. Enthält das Dokument dynamische Elemente müssen diese ebenfalls geparst und interpretiert werden.

Beispiele für DSLs sind eine Reihe von Konfigurationsdateien:
crontab, Makefile ,…
Neuere Systeme gehen dazu über die DSL in From von XML Dateien zu verarbeiten. Das hat den Vorteil, dass ein gewöhnlicher XML-Parser verwendet werden kann. Der Nachteil ist, dass die DSL nicht mehr frei ist und evtl. mehr Tipparbeit erforderlich ist (hohe Redundanz). Dynamische Inhalte müssen ebenfalls nachträglich interpretiert werden.

Interne DSL benutzen Komponenten der Wirtssprache  um die DSL zu parsen und auszuwerten. Die Vor/Nachteile sind genau umgekehrt zu den externen DSL.

In der Ruby Gemeinschaft werden, da Ruby selbst eine sehr freie Syntax hat, meist interne DSLs verwendet. An einem Beispiel soll die Implementierung einer DSL erläutert werden.

Der folgende Code stellt die Klassen Lexicon, Word und Wordform zur Verfügung. Damit soll der Aufbau eines einfachen Lexikons möglich sein.

class Lexicon
  def initialize
    @words={}
  end
  def add_word(word)
    @words[word.uname]=word
  end
  def show
    @words.each_value do |word|
      puts word
    end
  end
end
 
class Word
  attr_reader :uname, :forms, :categories
  def initialize(uname)
    @uname=uname
    @forms=[]
  end
  def add_form(form)
    @forms << form
  end
  def to_s
    forms.inject("#{uname}:\n") do |out, form|
      out << form.to_s << "\n"
    end
  end
end
 
class WordForm
  attr_reader :form, :categories
  def initialize(form, categories)
    @form=form
    @categories=categories
  end
  def is?(value)
    @categroies.values.include?(value.to_s)
  end
  def to_s
    "#{form} - #{categories.inspect}"
  end
end

Ein Lexikon lässt sich wie folgt aufbauen:

l=Lexicon.new
w=Word.new('gehen')
wf=WordForm.new('ging', :tempus => :imperfect, :numerus => :sg3, :genus => :m)
wf2=WordForm.new('ging', :tempus => :imperfect, :numerus => :sg1)
w.add_form wf
w.add_form wf2
l.add_word(w)

Zum Testen:

l.show
 
gehen:
ging - { :tempus => :imperfect, :numerus => :sg3, :genus => :m }
ging - { :tempus => :imperfect, :numerus => :sg1 }

Der erste Ansatz

Der erste Schritt zur Vereinfachung des Aufbaus soll wie folgt aussehen:

l=Lexicon.new do |lex|
  lex.word('gehen') do |word|
    word.form('ging', :tempus => :imperfect, :numerus => :sg3, :genus => :m)
    word.form('ging', :tempus => :imperfect, :numerus => :sg1)
  end
end

Dafür müssen die Klassen erweitert/ergänzt werden:

class Lexicon
  def initialize
   @words={}
   yield self
  end
  def word(*attr)
    wrd=Word.new(*attr)
    yield wrd
    add_word(wrd)
  end
end
class Word
  def form(*attr)
    frm=WordForm.new(*attr)
    add_form(frm)
  end
end

Drei Dinge sind an diesem Ansatz “unvorteilhaft”:

  1. Die Verknüpfung der Klassen untereinander durch Aufruf der Konstruktoren (Lexikon <-> Word und Word <-> WordForm). Der Nachteil wird bei der Vererbung der Klassen sehr deutlich.
  2. Die klare Struktur der Klassen wurde durch die Methoden, die für die DSL benötigt werden verunreinigt. Die DSL sollte auf dem Model aufsetzen und diese so wenig wie möglich beeinflussen.
  3. Die DSL ist noch immer “unschön”. Am besten wäre es, wenn das Lexikon sich wie folgt aufbauen liesse:
l=lexicon do
  word 'gehen' do
    form 'ging', :tempus => :imperfect, :numerus => :sg3, :genus => :m
    form 'ging', :tempus => :imperfect, :numerus => :sg1
  end
end

Der richtige Weg

Wir wenden uns also wieder der ursprünglichen Form zu und führen eine neue Klasse LexiconBuilder ein:

class LexiconBuilder
  attr_reader :lexicon
  def initialize(lexicon)
    @lexicon=lexicon
  end
  def word(*attr)
    @word=Word.new(*attr)
    yield
    @lexicon.add_word(@word)
    nil
  end
  def form(*attr)
    frm=WordForm.new(*attr)
    @word.add_form(frm)
    nil
  end
end

Dann benötigen wir noch die Definition von lexicon:

def lexicon(&block)
  lexikon=Lexicon.new
  builder=LexiconBuilder.new(lexikon)
  builder.instance_eval &block
  lexikon
end

Hier liegt ein wenig Ruby Magie in der Luft. Nachdem das Lexicon und der LexiconBuilder erzeugt werden, wird der übergebene Block im Kontext des Builders ausgeführt. Jegliche Methodenaufrufe erfolgen also auf der Instanz von LexiconBuilder.

LexiconBuilder übernimmt hier die Rolle des Parsers. Es stellt alle (word, form) syntaktischen Elemente der DSL bereit und baut die Struktur wie gewünscht auf.

Die Vorteile liegen auf der Hand:

  1. Keine Einführung neuer Abhängigkeiten in bestehenden Klassen
  2. Alle Ahängigkeiten sind in LexiconBuilder gekappselt, LexiconBuilder lässt sich austauschen um eine andere DSL zu unterstützen.

Dieser relativ einfache Builder hat den Nachteil, dass es die “Syntax”, in unserem Fall die Abfolge der Methodenaufrufe, nicht überprüft. So wäre das folgende Programm durchaus korrekt, würde aber falsche Ergebnisse liefern:

l=lexicon do
  word 'gehen' do
    word 'foobar' do
      form 'ging',:tempus => :imperfect, :numerus => :sg3, :genus => :m
      form 'ging',:tempus => :imperfect, :numerus => :sg1
    end
  end
end

Wie man sieht, ist der Aufbau von DSLs keine Raketenwissenschaft und sollte, wenn der Code “häßlich” wird, auf jeden Fall in Betracht gezogen werden.

Bookmark and Share

Schlagworte:

Autor: Peter Schrammel,

Ein Rails-Entwickler in München. Immer auf der Suche nach Hirnfutter.

Artikel bewerten:

1 Sterne2 Sterne3 Sterne4 Sterne5 Sterne (3 Bewertung(en), durchschnittlich: 5.00 (max. 5)
Loading ... Loading ...

2 Kommentare zu “Domänenspezifische Sprachen und Ruby”

  1. Roland Moriz schreibt:

    Sehr spannender Artikel – vielen Dank! :-)

  2. Stephan schreibt:

    Danke sehr – das ist doch elegant gemacht.