Zurück | Übersicht

Perl 6 Tutorial - Teil 8 : Introspektion und Metaprogrammierung

Herzlich willkommen auch zum achten und letzten Teil dieses tiefschürfenden Tutoriums für werdende Perl 6-Programmierer. Diesmal wird es sogar noch eine Ebene tiefer gehen, zu den Interna der Sprache und wie sie befragt und verändert werden. Da diese Bereiche stärker theorielastig sind und von Pugs und Rakudo bisher kaum implementiert werden, besteht diese Folge aus mehr Text und weniger Beispielen als üblich.

Wozu Metaprogrammierung?

Von Anfang an ging es bei diesem Projekt nicht nur darum vergangene Irrtümer auszubügeln und den aktuellen Stand dynamischer Sprachen in vielem wieder ein- und überholen. Perl 6 sollte auch nach längerer Reifung ebenfalls lange halten. Was bedeutet, daß die Sprache fähig sein muß, künftige Anpassungen zu unterstützen, ohne das die Implementation des Interpreters angefasst werden wird. Einfacher gesagt: Perl 6 sollte bessere Möglichkeiten der Metaprogrammierung erhalten, als die derzeit berüchtigten Sourcefilter, die abgeschafft wurden, da sie viel zu langsam und fehleranfällig sind, um in echtem Produktionscode eingesetzt zu werden

Das ganze hat aber noch eine anderen Grund. Viele der weitverbreiteten Sprachen haben etliche Varianten oder auch Ableger, da verschiedene Fachbereiche oder Anwendergruppen verschiedene Betrachtungsweisen kennen oder bevorzugen. Um das Absplittern von Derivaten wie es z.B. mit PHP geschah zu verhindern, muß Perl noch wandlungsfähiger werden. Und nur eine gemeinsame Basis macht eine Infrastruktur und damit alle Beteiligten wirklich mächtig (siehe CPAN). Gerade in den letzten Jahren wird das mit dem Modewort domain specific language (DSL) zusammengefasst, wenn Sprachen einer Aufgabenstellung angepasst werden können, um diese effektiv und für die mit der Materie Vertrauten verständlich zu lösen. Flexibilität (TIMTOWTDI) hatte sich Perl schon immer auf die Fahnen geschrieben und dies wird mit Perl 6 nur noch etwas weiter getrieben. Perl 6 ist in Wirklichkeit eine Metaprogrammiersprache, die sich zur Laufzeit in fast alles Denkbare verwandeln kann. Dies ist in Ordnung so, mein Larry Wall, da alles erlaubt sein soll, wenn man es vorher explizit deklariert. Und Lernende werden mit einem ausgewogenem Satz vordefinierter Befehle zu einem "guten Stil" angeleitet, bis sie fähig werden, alle Regeln zu verändern. Und je mehr sie lernen diese Regeln zu ihrem Vorteil zu ändern, umso kürzer, effektiver und oft auch lesbarer werden ihre Programme.

Dieser Ansatz bringt nicht nur Freiheit für den Programmierer sondern erlaubt die immer komplexer werdenden Softwaresysteme kompakter und beherrschbarer zu schreiben. Ein Blick in die Java-Welt zeigt die Nachteile hierarchisch-logischer Strukturen, die oft zu unbeweglich und überladen und damit schwer lesbar und veränderbar sind. Aber selbst in der Welt von Perl und Ruby lernte man in den letzten Jahren, daß die zuerst gepriesenen Web-Frameworks schnell sehr umfangreich werden. Jedes ist eine Welt für sich, die erlernt sein will, obwohl sich viel Funktionalität ähnelt. Deshalb entstanden in letzer Zeit mit Rack, Mojo und anderen Unterfangen Software, die sich auf einzelne Probleme konzentriert, um diese in logischen Einheiten mit eine möglichst einfachen API handhabbar zu halten. Die Idee dahinter ist nicht neu, denn Perl 1.0 brachte Shellbefehle in die C-Syntax brachte, um komplexere Probleme kurz und und dem menschlichen Denken nah zu lösen. Später erfüllten in Perl 5.n gute Module den gleichen Zweck, nur das sie bereits von Programmierern der Sprache zugefügt werden konnten, ohne den C-Quellcode von Perl anzufassen. Das neue Perl 6 ist eine logische Fortsetzung dieser Entwicklung zu einer Sprache die quasi "flüssig" ist, und in verschiedene Situationen die Syntax annehmen kann, der dem Ideal des Benutzers entspricht. Dadurch fallen Barrieren, die bisher durch Programmiersprachgrenzen aufgestellt wurden. Große Systeme bestehen zunehmend aus Teilen die in verschiedenen Sprachen und Versionen geschrieben wurden, die sich nur in Details unterscheiden können, aber dennoch von zusätzlicher Middleware "zusammengehalten" werden müßen. Diese Teilsystem werden oft kaum mehr angefasst, da sie ausgereift, zuverläßig aber nicht immer leicht wartbar und auch nicht leicht erneuerbar sind. Deshalb wachsen diese Flickenteppiche zu ungeahnten Komplexitäten und es gibt bereits Lehrstühle für Professoren, die Menschen beibringen wollen mit solchen Ungetümen umzugehen. Besser wäre es doch, wenn solche Software gleich in Perl 6, vielleicht auch Ruby oder Lisp geplant wird, um diese Komplexität nicht entstehen zu lassen. Und selbst wenn "unberührbare" Software Teil einer Anwendung ist, kann eine sehr anpassungsfähige Sprache Kommunikation mit wenig Programmieraufwand ermöglichen, und Perl bleibt was es immer war: eine glue-language.

Wo beginnt Metaprogrammierung?

Wenn eine kleine sub den verfügbaren Wortschatz erweitert, ist das bereits Metaprogrammierung? Das kommt auf den Fall an. Subroutinen können dazu verwandt werden etwas Ordnung in Spaghetticode zu bringen, was unter das Schlagwort "strukturierte Programmierung" fällt. Da die Errungenschaften von if, while und Routinen seit den 70er-Jahren Allgemeingut wurden, fällt der Begriff heute selten. Wesentlich häufiger hört man dieser Tage dagegen das Wort "funktionale Programmierung". So heiß ein Programmierstil der sehr abstrakt ist und daher in die Metaprogrammierung hineinragt. Mark Jason Dominus beschreibt in "Higher Order Perl" genauer wie das in Perl aussieht (Buchrezension in $foo Winter/2008). Das Buch ist wohl zum Teil nach den "Higher Order Functions" benannt. Das sind Routinen die eine Aufgabe sehr abstrakt lösen und deshalb sehr vielseitig einsetzbar sind. Viele Perlianer verwenden bestimmt solche "Higher Order Functions", ohne den Begriff zu kennen. Z.B. map und grep fallen in diese Kategorie. Aber mit Perl 5.0 konnte man auch bereits selber solche Functionen schreiben, da schachtelbare Namensräume für Variablen und Codereferenzen als Parameter anwendbar waren. Ähnlich der OOP war in Perl 5 fast alles möglich, nur vieles jetzt einfacher. Eine wichtige Technik in der funktionalen Programmierung ist z.B. das Currying. Auf deutsch: das Erstellen einer Codereferenz auf eine Funtion höherer Ordnung (einen Alias), bei der bestimmte Parameter festgelegte Werte haben und nur die restlichen Parameter bestimmt werden können. Also wenn eine Funktion potenz beliebige Potenzen berechnet (ja es ginge auch mit ==, aber folgender Code entspricht einem funktionalem Lösungsansatz):

   subset Num+ of Num where { $_  > 0 };
   subset Num- of Num where { $_  < 0 };
   subset Zero of Num where { $_ == 0 };

   multi sub potenz (Num :$basis!, Zero :$exponent!) {
      return 1;
   }

   multi sub potenz (Num :$basis!, Num- :$exponent!) {
      return 1 / potenz( $basis, -$exponent);
   }

   multi sub potenz (Num :$basis!, Num+ :$exponent!) {
      return $exponent * potenz( $basis, $exponent - 1 );
   }

Ein Funktion quadrier würde ich nun explizit wie folgt erhalten:

   &quadrier := &potenz.assuming( exponent => 2 );
   # alternative Schreibweise:
   &quadrier := &potenz.assuming( :exponent(2) );

Die läßt sich wie erwartet aufrufen:

   say quadrier(5);     # ergibt 25

.assuming (englisch für angenommen) drückt in einer Alltagssprache genau aus worum es hier geht: quadrier entspricht potenz, unter der Annahme, daß der Exponent 2 ist. Das := ist keine Zuweisung sondern ein binding. P6 kennt weder direkten Manipulation der Symboltabelle mit Typeglobs noch Referenzen (der Schrägstrich ( backslash) erzeugt jetzt captures, siehe Teil 6). Um einen Alias auf den Inhalt einer Variable zu erhalten, bindet man diese Variable mit := an eine Andere.

   my $planeten = 7;
   my $planety := $planeten;
   $planeten = 9;
   say $planety;     # gibt 9
   say "yes" if $planety =:= $planeten;

Das letzte Beispiel führt die Ausgabe aus, da die Variablen an das gleiche Objekt gebunden sind. Ein Binden zur Kompilierungszeit mit der Schreibweise ::= mag selten gebraucht werden, aber das Ausführen von Routinen zum frühstmöglichen Zeitpunkt wesentlich öfter. In Perl 5 schrieb man dazu den Code in einen BEGIN-Block, was weiterhin möglich ist, aber Perl 6 kennt noch ein anderes Konzept, das dem ähnelt, aber weitaus mächtiger ist. Manche behaupten sogar, daß vor allem diese Macros LISP mächtiger machen als alle anderen Sprachen, was in den 60er und 70er Jahren auch gestimmt haben mag.

Die wunderbare Welt der Macros

Macros sind (wie angedeutet) Routinen, die beim Kompilieren ausgeführt werden. Im Gegensatz zu einer sub, ändern sie die Sprache, da sie statt eines Wertes, einen AST (Baumstruktur, die als Zwischenform beim Kompilieren entsteht) liefern, der beim Kompilieren anstelle jedes Macroaufrufes in den AST des Programmes eingefügt wird, bevor die Ausführung beginnt.

In C ist das bei weitem einfacher. Hier sind Macros lediglich Textbausteine die an die mit dem Makronamen markierten Stellen im Quellcode eingefügt werden, bevor der Kompiler sein Werk beginnt. Die alten Perl 5-Sourcefilter taten ihr Unwesen nach dem gleichen Prinzip, nur daß hier die volle Kraft der P5-Regex am Werke war. Das kann subtile, schwer zu entdeckende Fehler erzeugen, da jeder Macroprozessor blind für die Bedeutung der Sprachsyntax ist und der von ihm erstellte Quelltext nirgends einsehbar ist. Perl 6-Macros haben standardmäßig (wie alle Blöcke) keine Seiteneffekte auf die lexikalische Struktur der umgebenden Quellen. So etwas nennt die Fachwelt: hygienische Macros. Folgende Makros sind hygienisch:

   macro summe {  3 + 4  }
   macro summe { '3 + 4' }
   macro summe is parsed { 3 + 4 }

parsed ist ein Trait von Routinen das nur Macros wirklich benötigen, aber da es default ist, kann es auch weggelassen werden. Und auch die Verwendung der Macros ist denkbar einfach.

   say 2 *  summe;    # 14
   say 2 *  summe();  # 14
   say 2 * &summe();  # 14

Auch wenn alle Aufrufe 14 ergeben, so sind sie nicht identisch, da der dritte erst zur Laufzeit aufgelöst wird. Doch manchmal sind dreckige Macros genau was man möchte und dann schreibt man.

   macro summe is reparsed {  3 + 4  }

Wird dieses Macro im vorigen Beispiel aufgelöst, hieße das Ergebnis 10. Denn dieses Macro gibt nur einen String zurück, der zusammen mit dem umgebenden Quellcode compiliert wird. Dabei gilt die alte Regel: Punkt- vor Strichrechnung. Da diese Funktionsweise derartige Probleme provoziert, hat sie die beschriebene Kindersicherung und es wird standardmäßig von Macros ein AST zurückgegeben. Aber auch außerhalb von Macros kann dies getan werden. quoting mit dem Adverb :code (siehe letzte Folge) und ein quasi vor geschweiften Klammern oder Anführungszeichen kann das ( quasiquoting) bewirken.

   return quasi { say "foo" };
   return Q :code / say "foo" /;

Beide Zeilen liefern keine Referenz auf eine Routine sondern ein kompiliertes Stück Programm. Folglich ist ein Macro eine zu BEGIN ausgeführte Routine deren Rückgabewert derart quasi kommentiert ( gequoted) ist. Dies ist eine sehr elegante Eigenschaft von Perl 6, daß jedes Sprachelement mit den Bordmitteln der Sprache beschrieben werden kann. Deswegen kann die Syntax, so reichhaltig sie auch scheinen mag, auf einen wesentlich kleineren Kern reduziert werden, aus dem sich beliebige Sprachen aufbauen lassen. Deshalb gilt für Perl 6 was John Foderaro einst über Lisp sagte: "es ist eine programmierbare Programmiersprache". Weil Perl syntaktisch viel reichhaltiger als Lisp ist, das nur Funktionsnamen, Wertelisten und runde Klammern kennt, müßen Perl-Macros wesentlich mehr können, um wirklich alle Sprachbestandteile erweitern zu können. Einen neuer Operator (z.B. für die mathematische Fakultät-Funktion) wird so erzeugt:

   macro postfix:<!>   { [*] 1..$^n }
   macro postfix:('!') { [*] 1..$^n }

Das gewählte Operatorsymbol kann auch in andere Klammern als den Spitzen gehüllt sein, aber der Vorsatz "postfix:" ist entscheidend, da wir einen Postfix-Operator, also einen nachgestellten Operator (wie in $p++) definieren wollen. Um noch zu bestimmen wo der neue Operator seinen Platz in der Vorrangtabelle hat, könnte man hinzufügen:

   macro postfix:<!> is equiv(&postfix:<++>) { ... }

Das könnte man auch mit is tighter oder is looser definieren. Dann bekäme der Operator eine eigene Spalte in der Vorrangtabelle die jeweils über oder unter dem angegebenen Operator liegt.

Diesem Schema entsprechend gibt es eine lange Reihe von Schlüsselwörter die jede Art von Operator oder Schlüsselwort bezeichnen. Es lassen sich eigne Arten des Quoting, Regex-Befehle, Spezialvariablen oder neue sekundäre Sigils einführen. Wenn jemand XML-Kommentare in seinem Perl haben möchte so reicht ein:

   macro circumfix:«<!-- -->» ($text) is parsed / .*? / { "" }

$text ist der Parameter, der den Text zwischen den Kommentarzeichen beinhaltet. Nach dem is parsed steht die Regex mit der geparsed werden soll.

Da entlang Alice

Aber die Manipulation der Sprache kennt noch eine Ebene. Es ist sogar möglich die Regeln zu ändern mit denen der Interpreter den Quellcode einliest. Seine Arbeitsweise wird in der STD.pm mit der Hilfe von P6-Regex-Grammatiken definiert. Wie diese formuliert werden und aufgebaut sind, behandelte die letzte Folge.

Intern wird die Sprache in mehrere Teilsprachen ("slangs") gegliedert. Die Kernsprache ist z.B. eine Grammatikobjekt, auf das mit $~MAIN zugegriffen werden kann. Die Regeln für das Kommentieren stehen unter $~Q. Und die Regex die bestimmen wie Regex einzulesen sind, finden sich unter $~Regex. ~ ist die Twigil dieser Sondervariablen. Und da Grammatiken nach außen normale Objekte sind, können sie abgeleitet und verändert werden wie jedes andere Objekt auch. Sämtliche Änderungen gelten nur für den aktuellen Namensraum ( scope) und die defaults können jederzeit wiederhergestellt werden, da sie unter %?LANG gespeichert sind.

Zeig mir dein Inneres!

Objekte können in Perl 6 sehr stark durchleuchtet und verändert werden. Jede Elternklasse, jede Methode und jede Signatur (mit $obj.methode.signature) kann abgefragt oder verschiedenst geprüft werden. $obj.WHERE nennt die Speicheradresse, .WHAT den Typ, was in etwa dem Befehl ref in Perl 5 entspricht.

Es lassen sich mit Roles beliebige Methoden zu Laufzeit in ein Objekt einfügen, aber der direkteste Weg dafür ist wohl:

   augment slang Regex {
      token regex_metachar:<^> { ... }
   }

augment (englisch für einblenden) fügt nur Regeln (Methoden) in die Grammatik (Klasse) ein, soll eine gesamte Slangdefinition ausgetauscht werden, dann ist supersede (englisch für überlagern) das Mittel der Wahl.

Es gibt noch viele weitere Sondervariablen, wie für den umgebenden Block ( &?BLOCK) oder die umgebende Routine ( &?ROUTINE) mit denen sich weit mehr tun läßt als noch in Perl 5, z.B. lassen sich alle Sprungmarken im aktuellen Block mit &?BLOCK.labels auflisten. Auch &?ROUTINE.name ist praktisch, wenn man nicht merh weiß wo sich die Ausführung gerade befindet. Doch dies sieht man alles in den Tabellen im Anhang B der Perl Tafeln.


Zurück | Übersicht

-- HerbertBreunung - 02 Jun 2009
Topic revision: r7 - 2009-12-02 - 23:24:28 - HerbertBreunung
 
Bitte die NutzungsBedingungen beachten.
Bei Vorschlägen, Anfragen oder Problemen mit dem PerlCommunityWiki bitten wir um WebBottomBarExample">Rückmeldung.