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.
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 { get; set; }
[Obsolete("Use Body property instead of Block."), XmlIgnore]
public Block Block { get; set; }
public Block Body { get; set; }
public string Name { get; set; }
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.