Advancing Boo – Metaboolisch gut

Nicht nur die japanischen Metabolisten versuchten sich am urbanen Kontext eines großen Systems. Die Massengesellschafft .NET programmiert in C# fromm nach OOP, TDD, DDD, CCD und wie die Paradigmen alle heißen. Da kommt ein kleiner Zwerg Names Boo, will mit uns Großes leisten und Assemblies bauen – als lebendiger Prozess ausschließlich durch flexible und erweiterbare Strukturen. Gemäß dem Vorbild reichen hier die altehrwürdigen Gesetze von Form und Funktion nicht mehr aus. Es müssen neue geschaffen werden! Welche Gesetze des Raumes und des Funktionswandels als Anforderung an die neue .NET Kultur sind damit eigentlich gemeint?

Revolution schreien die Einen, Blasphemie die Anderen. Entscheidet euch selbst. Ich werde versuchen den Syntax-Besteckkasten des Assemblyarchitekten zu erweitern und zu zeigen wie ihr ihn einsetzen könnt.

Angefangen hatte in Boo alles mit ein paar kleinen Interfaces. Im ersten Artikel der Reihe Advancing Boo habe ich versucht euch die Grundlagen über das Compilerhosting, die Parametrisierung sowie die CompilerPipeline näher zu bringen. Die erste ICompilerComponent haben wir damit also beleuchtet. Nun schauen wir uns doch mal das 1 mal 1 der Baumschule genauer an:

IAstMacro 
IAstAttribute
IAstGeneratorMacro

IAstMacro und IAstGeneratorMacro sind sich sehr ähnlich. Sie bekommen ein MacroStatement und müssen dieses in ein neuen Knoten vom Syntaxbaum umwandeln. Wer sich schon einmal den Syntaxbaum von Boo angesehen hat, wird festgestellt haben, dass die Mutter aller Anweisungen ein Knoten, nämlich der Typ Node ist – ziehmlich logisch, wie ich finde, handelt es sich doch bei Compilern in der Abstrakten Darstellung immer um einen Baum. Der Unterschied zwischen den Macros liegt darin, das der Generator – denken wir mal an Generatoren in C# – per yield Schlüsselwort eine Liste von Knoten zurück liefert. Das IAstAttribute wird auf einen Knoten gesetzt (AOP style a la PostSharp).

public interface IAstMacro : ICompilerComponent
{
  Statement Expand(MacroStatement statement);
}

public interface IAstGeneratorMacro : ICompilerComponent
{
  IEnumerable<Node> ExpandGenerator(MacroStatement statement);
}

public interface IAstAttribute : ICompilerComponent
{
  void Apply(Node targetNode);
  Attribute Attribute { set}
}

Was müssen wir tun? Von einem der Interfaces ableiten und Methoden implementieren. Hört sich sehr nach einfach an? Ist es auch, zumindest in der Theorie. In der Praxis muss der Umgang mit dem Baumbesteck erst einmal geübt werden. In unserer urbanen Welt werden wir aber dann doch eine abstrakte Implementierung als Basisklasse nutzen um das Rad nicht immer wieder neu zu erfinden. Wie funktioniert das aber? Der Compiler durchsucht den Text nach Tokens. Trifft er dabei auf einen ihn unbekannten Ausdruck, so wird in der Liste der von den Macro-Interfaces abgeleiteten Klassen nachgesehen ob sich dieser Ausdruck dort finden lässt. Per Konvention setzt sich dabei z.B. ein, neues Schlüsselwort lock aus LockMacro zusammen. Sprich, das Schlüsselwort + “Macro” als CamelCase.

image

Das Macro wird per Expand Methode von der CompilerPipeline aufgerufen um die Transformation des Codes durchzuführen. Der auf den unbekannten Ausdruck folgende Syntaxknoten wird als Argument an die Expand Methode gegeben. Daraus lässt sich ableiten, dass der Inhalt der dem Schlüsslewort folgt immer gültige Boo-Syntax ergeben muss. Das ist nach diesem Konzept verständlich, aber auch schade, da z.B. eine Erweiterung wie XML-Literale in VB.NET so ohne weiteres nicht machbar sind.

public ExpressionCollection Arguments { getset}
[Obsolete("Use Body property instead of Block."), XmlIgnore]
public Block Block { getset}
public Block Body { getset}
public string Name { getset}
public override NodeType NodeType { get}

Arguments ist dann nützlich, wenn dem Schlüsselword eine Argumentliste, ähnlich wie bei Funktionen folgt. Wer diesen Weg ein gutes dutzend mal gemacht hat wird ein immer wiederkehrendes Muster erkennen und sich nicht nur auf Grund der Namenskonvention fragen: “Geht das auch einfacher?”. Vermutlich schon und deshalb gibt es das MacroMacro. Die Benutzung des Macros wird abstrakt so beschrieben:

"Usage: `macro [<parent.>+]<name>[(arg0,...)]`";

soll heißen, macro gefolgt von einem optionalen Parent MacroName gefolgt von dem eigentlichen Macronamen gefolgt von einer Optionalen Parameterliste.

z.B.:

macro parent:
    #hier passiert was 
macro parent.child:
    #hier passiert noch mehr

#wohingegen früher geschrieben werden musste:
macro parent:
    macro child:
        #...

bzw. mit Parametern:

macro test (first as string, second as int, third as double*):
    # double* heißt soviel wie IEnumerable[of double], in C# IEnumerable<double>
    # gefolgt von der transformation

 

Für die Transformationen in den Macros müssen wird dann umständlich die Klassen aus dem Syntaxbaum nutzen um das zu tun, was uns sonst der Compiler abnimmt, erstellen einer abstrakten Baumstruktur. Aber zum Glück gibt es da doch Abhilfe die ich schon in meinem Einleitenden Artikel erwähnt hatte: Der Oxford operator [| |] aka QuasiQuotation und der so genannte Splicing Operator $. Mit QuasiQuotation nimmt uns der Compiler wieder genau das ab was wir gerade mühevoll in Handarbeit machen sollen: Das erzeugen eines AST. Der Operator ist zur Compilezeit aktiv! Bitte nicht vergessen, weil das für das Verständnis im Code wichtig ist. Das Macro bekommt z.B. eine Liste von Namen und soll somit Klassen erzeugen. Das ganze gleicht dann Template Systemen wie T4, nur eben auf Codeebene, daher auch der Name Code Templates. Hier nun ein Beispiel:

Um mit dem Typsystem etwas besser vertraut zu werden habe ich mir eine Hilfsfunktion gebaut:

def show( node as Node):
    print "CodeString:\n ${node.ToCodeString()}\n"
    print "NodeType:\t ${node.NodeType}" 
    print "Expression:\t ${node}\n"

Danach können wir nun uns die Macros etwas genauer betrachten:

macro generate_klasses(names as string*):
    count = 1
    for name in names:
        postfix as string
        match count:
            case 1:
                postfix = "st"
            case 2:
                postfix = "nd"
            case 3:
                postfix = "rd"
        yield [|
            class $name(object):
                def constructor(a as int):
                    print "from ${ToString()}, created ${$(count++ + postfix)} with ${a}"
                def $(name + "Me")():
                    pass
        |]

generate_klasses "click""clack""clock"

show [| generate_klasses "click""clack""clock" |]

print "creating generated class instances:"
clock_inst = clock(10)
clack_inst = clack(5)
click_inst = click(0)
print Environment.NewLine

click_inst.clickMe()
clack_inst.clackMe()
clock_inst.clockMe()

Was macht das Macro hier? Für eine Liste von String erzeuge pro Eintrag eine Klasse mit dem Namen des Eintrages das von object ableitet. Zusätzlich wird eine Funktion erzeugt die den Namen + Me enthält. Splicing und Quasiquotation verhalten sich erwartungsgemäß auch nicht anders als in anderen Programmiersprachen (siehe z.B. F#).

 

Das ganze kann Schlussendlich auch noch mit PatternMatching kombiniert werden, was uns noch mehr Gestalltungsmöglichkeiten verschafft. Mehr davon im nächsten Artikel.

DotNetKicks-DE Image
Published Mittwoch, 20. Oktober 2010 00:40 von Rainer Schuster
Abgelegt unter: , , , ,

Kommentare

# re: Advancing Boo – Metaboolisch gut

Montag, 25. Oktober 2010 19:36 von Robert Mischke

Wo führt das hin. Wo kann ich das praktisch einsetzen? Loht der Einsatz? Wo kann ich das in meinem LOB Projekt mit gutem gewissen verwenden. (Lohnt der Kauf von Ayendes Buch?) Viele Fragen :-)

# re: Advancing Boo – Metaboolisch gut

Donnerstag, 28. Oktober 2010 08:52 von Rainer Schuster

Es kann Sinn machen ja. Ob der Einsatz lohnt wirst du für dich selbst herausfinden müssen.

Immer da wo du ein Regelwerk konfigurieren musst, kann der Einsatz lohnenswert sein. Dort, wo Abstraktion noch mehr Sinn macht.

Das Buch kann ich für diejenigen Empfehlen, die eine nette Lektüre zum Lesen wollen und daran interessiert sind, wie eine DSL in der Realen Welt implementiert wurde. Wer an Boo interessiert ist, wird hier nur wenig finden. Ayende geht kurz auf die Basics ein und wie die Compilerpipeline bedient wird.

Wer auf Code steht, sollte sich lieber gleich Rhino.DSL aus seinem Github Repo ziehen und dort stöbern. Das ist wesentlich ergiebiger.

Kommentar abgeben

(verpflichtend) 
(verpflichtend) 
(optional)
(verpflichtend)