Programming languages from hell Proseminar im Sommersemester
Transcription
Programming languages from hell Proseminar im Sommersemester
Programming languages from hell Proseminar im Sommersemester 2010 Common Lisp Thomas Reschenhofer Technische Universität München 4.6.2010 Zusammenfassung Common Lisp ist zwar eine im ANSI Standard spezifizierte Sprache, lässt sich aber vor allem durch ihr mächtiges Makrosystem einfach an bestimmte Kontexte angepassen Zudem sind Konzepte wie Reflection (in Common Lisp u.a. durch das Metaobject Protocol möglich) in Common Lisp in einem ganz anderen Ausmaß in die Sprache eingearbeitet als beispielsweise in Java, während Konzepte wie dynamic binding und method dispatching in Sprachen wie Java gar nicht zum Einsatz kommen. 1 Einleitung/Kategorisierung Common Lisp ist eine multiparadigme Programmiersprache, die funktionale Elemente (Rekursion, Funktionen höherer Ordnung) enthält sowie imperative und objektorientierte Programmierung ermöglicht. Funktionen werden in Common Lisp strikt ausgewertet, d.h. bevor die Funktion ausgeführt wird, werden deren Parameter ausgewertet. Desweiteren gehört Common Lisp zur Gruppe der dynamisch getypten Programmiersprachen, was bedeutet, dass der Typ einer Variable erst zur Laufzeit überprüft wird. Außerdem unterstützt Common Lisp Metaprogramming, d.h. die Sprache kann sich selbst ändern/anpassen, so dass es möglich ist, kontextspezifische Sprachen (domain specific languages) zu implementieren. 2 2.1 Konzepte in Common Lisp Die Syntax und deren Anpassbarkeit Listing 1 zeigt einführend einige Beispiele für gültige und ungültige Common Lisp Programmzeilen. 1 Abbildung 1: Aufgaben von reader und evaluator. (+ 4 5 ) ; A d d i t i o n d e r Zahlen 4 und 5 (+ 4 5 ; k e i n e s c h l i e ß e n d e Klammer −> u n g ü l t i g e S−Expression ( quote (+ 4 5 ) ) ; d e r u n a u s g e w e r t e t e Ausdruck (+ 4 5) ’(+ 4 5 ) ; das s e l b e wie v o r i g e Z e i l e , ” S y n t a c t i c Sugar ” ( foo 4 5) ( f o o : p ” abc ” ) ; A u f r u f d e r Funktion f o o mit den Parametern 4 und 5 ; A u f r u f d e r Funktion f o o mit dem Keyword−Parameter : p () ; Sowohl l e e r e L i s t e , n i l a l s auch f a l s e Listing 1: Beispiele für Lispforms Eine Besonderheit von Common Lisp ist die besonders abstrakte Syntax: Jede Common Lisp Codezeile ist entweder eine Liste oder ein Atom. • Listen werden mit einer öffnenden Klammer eingeleitet und mit einer schließenden Klammer abgeschlossen, deren Elemente werden durch einfache Leerzeichen getrennt. Beispiel: (+ 1 2). Diese Listen stellen in Common Lisp Aufrufe von Funktionen, speziellen Operationen und Makros in Präfixnotation dar. • Ein Atom ist im Prinzip alles, das keine Liste ist, also Zahlen, Strings, Symbole, usw. Aus diesen Codezeilen werden vom sog. reader abstrakte Syntaxbäume erzeugt, welche anschließend vom evaluator ausgewertet werden. Abbildung 1 stellt diese Vorgänge für den Ausdruck (+ 1 2) in abstrahierter Form dar. Code, der vom reader akzeptiert wird, wird als S-Expression bezeichnet, vom evaluator akzeptierter Code als Lisp forms. Dies alleine vermag noch keine Besonderheit zu sein. Die Fähigkeit von Common Lisp, durch (wiederum in Common Lisp verfasste) Makros den reader und den evaluator zu beeinflussen und S-Expressions beliebig zu manipulieren und generieren, hebt Common Lisp allerdings von anderen Programmiersprachen deutlich hervor. Das später noch erwähnte Common Lisp Object System ist ein Resultat genau dieser Fähigkeit. 2 Alle Funktionen/Operationen/Makros, die reader und evaluator einsetzen, um ihre Arbeit zu verrichten, sind ganz gewöhnliche Common Lisp Funktionen/Operationen/Makros und können genauso gut vom Programmierer selbst verwendet werden, um beispielsweise selbst kleine Teilausdrücke auszuwerten (mit der Funktion eval ). 2.1.1 Funktionen in Common Lisp Funktionen werden in Common Lisp als Listen dargestellt, wobei das erste Element der Name der Funktion und alle restlichen deren Parameter darstellen. Wie erwähnt, erfolgt die Auswertung in Common Lisp strikt, d.h., bevor die Funktion ausgewertet wird, werden erst alle Parameter ausgewertet. Die Definition einer Funktion wird mithilfe des defun - Makros realisiert (siehe Listing 2). Dieser Makro erwartet als Parameter den Bezeichner der zu definierenden Funktion, eine Parameterliste und deren Implementierung. ( defun s q a r e ( x ) (∗ x x ) ) ; A u f r u f d e r Funktion ( square 5) ;−> 25 Listing 2: Definition der Funktion sqare Neben den ganz gewöhnlichen Parametern existieren noch optionale Parameter, die beim Aufruf nicht angegeben werden, Rest Parameter so dass die Funktion beliebig viele Argumente akzeptiert, und Keyword Parameter, also benannte Parameter. 2.1.2 Special Operators Nicht alle Operationen können auch als Funktionen realisiert werden, da es notwendig sein kann, dass nicht alle Parameter vor der Funktion ausgewertet werden. Aus diesem Grund definiert Common Lisp eine Hand voll sog. Special Operators (Hier nur die wichtigsten): if Die Bedingungsverzweigung. Syntax: (if test-form then-form else-form) quote Dieser Special Operator erwartet einen Ausdruck als Parameter und liefert diesen unausgewertet zurück. (quote (+ 1 2)) wertet also nicht die Addition aus, sondern liefert den Ausdruck (+ 1 2). (quote any-expr) ist äquivalent zu ’any-expr. Quasi das Gegenstück dazu ist die Funktion eval, welche einen beliebigen Common Lisp Ausdruck auswertet. 3 function Mithilfe dieses Special Operators erhält man zu einer gegebenen Funktion das Funktionsobjekt, welches beispielsweise als Parameter einer anderen Funktion übergeben werden kann und somit Funktionen höherer Ordnung erlaubt. (function foo) ist äquivalent zu #’foo Das Gegenstück dazu ist die Funktion funcall (bzw. apply), welche eine Funktion gegeben durch ihr Funktionsobjekt ausführt. let Der Special Operator let erzeugt eine neue Variablenbindung. Aufgrund dessen, dass Common Lisp das Konzept der dynamischen Variablenbindung unterstützt, kann dies Auswirkungen auf die Laufzeitumgebung haben. Dies ist der Grund, wieso let als Special Operator geführt wird. let erwartet als ersten Parameter eine Liste von einzuführenden Parametern, gefolgt von einer beliebigen Anzahl von Lisp-Forms, welche gleichzeitig den Gültigkeitsbereich der eingeführten Variablen darstellen. ’(+ 1 2 ) ; −> Der Ausdruck (+ 1 2) ( eval ’(+ 1 2 ) ) ; −> 3 #’ s q u a r e ; −> F u n k t i o n s o b j e k t zu s q u a r e ( f u n c a l l #’ s q u a r e 5 ) ; −> 25 Listing 3: Die Special Operators quote und function und ihre Pendants 2.1.3 Makros in Common Lisp Rein theoretisch könnten all diese Special Operator durch Makros ersetzt werden. Listing 4 zeigt dies anhand der Bedingungsverzweigung. Bei dem Zeichen, das dazu dient, den Makro cond nicht auszuwerten, handelt es sich nicht um den zuvor eingeführten Special Operator quote, sondern um den Makro backquote, welcher im Gegensatz zum Special Operator quote die Möglichkeit bietet, Teilausdrücke auswerten zu lassen. ( defmacro i f f ( test−form then−form e l s e − f o r m ) ( l i s t ‘ cond ( l i s t test−form then−form ) ( l i s t t else−form ) ) ) Listing 4: Beispielhafte Implementierung von if als Makro Obwohl die Definition von Makros der Definition von Funktionen sehr ähnlich sieht, verhalten sich Makros komplett anders. Makros werden in der sog. Macro Expansion Time ausgewertet (Die Auswertung eines Makros besteht im Wesentlichen darin, Code zu erzeugen bzw. manipulieren). Das ist der Zeitpunkt, zu dem der Compiler auf einen Makroaufruf stößt, also während der Compiletime 4 und noch vor Runtime. Dies hat zur Folge, dass Makroparameter keine Laufzeitdaten darstellen, sondern Sourcecode, der vom Makro bearbeitet werden kann. Als Beispiel: Ein Makro, der scheinbar einen mathematischen Ausdruck in Infixnotation auswertet, macht im Grunde nichts anderes, als die Operatoren und Operanden anders anzuordnen und den Infixausdruck durch den äquivalenten Präfixausdruck zu ersetzen. Listing 5 definiert dazu einen einfachen Makro, der beispielsweise den Ausdruck (simple-infix 1 + 2) in den Präfixausdruck (+ 1 2) umwandelt. ( defmacro s i m p l e − i n f i x ( operand1 o p e r a t o r operand2 ) ( l i s t o p e r a t o r operand1 operand2 ) ) Listing 5: Implementierung eines Infix-Präfix - Makros 2.1.4 Reader-Makros Noch anschaulicher wird die Macht des Makrosystems, wenn das Beispiel der Infix-Notation noch weiter geführt wird. Und zwar mithilfe sog. Reader-Makros, welche ausgeführt werden, bevor der reader seine Arbeit verrichtet. In Listing 6 werden 2 Reader-Makros definiert, welche in eckigen Klammern stehende Ausdrücke in Infix-Notation auswerten. Der Aufruf (simple-infix 1 + 2) ist folglich äquivalent zum Aufruf [1 + 2]. Der erste Reader-Makro kümmert sich um die öffnende, eckige Klammer, wobei eine Liste aller bis zur nächsten schließenden, eckigen Klammern auftauchenden S-Expressions als Stream übergeben wird. Diese Liste wird aus dem Stream ausgelesen und deren Elemente wie zuvor beim simple-infix - Makro anders angeordnet. Der zweite Reader-Makro ersetzt anschließend die schließende, eckige Klammer durch eine schließende, runde Klammer. ( set−macro−character #\[ # ’(lambda ( stream char ) ( l e t ( ( s e x p r ( read−delimited−list #\] stream t ) ) ) ( l i s t ( second s e x p r ) ( f i r s t s e x p r ) ( third s e x p r ) ) ) ) ) ( set−macro−character #\] ( get−macro−character #\))) Listing 6: Implementierung eines Reader-Makros Obwohl der Aufruf eines (normalen) Makros auf den ersten Blick nicht von jenem eines Special Operators oder einer Funktion zu unterscheiden ist, sind diese 3 Typen grundverschieden. Wärend die Definition neuer Funktionen auch tatsächlich nichts anderes ist wie in anderen Programmiersprachen wie z.B. Java auch, haben Makros ganz andere Auswirkungen auf das Programm bzw. die Sprache. So befähigt das Makrosystem von Common Lisp den Programmierer beispielsweise, die Sprache an bestimmte Kontexte anzupassen, neue Kontrollstrukturen einzuführen (z.B. bestimmte Schleifentypen) und die Syntax der Sprache (z.B. mit Reader-Makros) zu manipulieren. 5 2.2 Variablenbindung in Common Lisp - Dynamic Binding Folgendes Szenario soll diesem Thema als Einleitung dienen: Sie wollen in einem bestimmten Gültigkeitsbereich eine neue Variable x einführen. Es existiert jedoch in einem übergeordneten Gültigkeitsbereich bzw. global bereits eine gleichnamige Variable x’. Wie geht die Sprache damit um? Hierbei handelt es sich um die Frage der Variablenbindung, wobei grundsätzlich folgende 2 Arten unterschieden werden: • lexikalische (statische) Bindung • dynamische Bindung Während bekannte Sprachen wie C und Java ausschließlich die lexikalische Variablenbindung unterstützen, ist in Common Lisp zusätzlich die Definition dynamisch gebundener Variablen möglich. 2.2.1 Lexikalische Bindung Ist die Variable x’ aus dem einführenden Szenario eine lexikalisch gebundene Variable, so wird mit der Definition der lokalen Variable x auch tatsächlich eine neue Variable erstellt, welche x’ überdeckt, solange sich das Programm im Gültigkeitsbereich von x befindet. Nach Verlassen des Gültigkeitsbereiches von x wird die Variable wiederum zerstört. ; Definition einer Variable x , als x ’ bezeichnet ( let (( x 1)) ; D e f i n i t i o n d e r Funktion foo , w e l c h e den Wert von x z u r ü c k g i b t ( defun f o o ( ) x) ; Funktionsrumph ; Definition einer Variable x ( let (( x 2)) ; A u f r u f von f o o ( f o o ) ) ) ; −> 1 Listing 7: Beispiel für lexikalische Bindung Listing 7 imitiert das oben genannte Szenario mithilfe der lexikalischen Variablenbindung. Zwar wird die Funktion foo in einer Umgebung aufgerufen, in welcher x’ durch x überdeckt wird und somit die Variable den Wert 2 besitzt. Im Funktionsrumpf von foo wird x’ keineswegs überdeckt, sodass hier die Variable folgerichtig den Wert 1 zurückgibt. Wie gesagt ist dies das Verhalten, welches man vom Programmiersprachen wie Java und C gewohnt ist. 6 2.2.2 Dynamische Bindung Handelt es sich im Einführungsszenario bei x’ um eine dynamisch gebundene Variable, so wird bei der Definition der Variable x gar keine neue Variable eingeführt, sondern der neue Wert an die existierende Variable x’ gebunden. Diese Bindung wird nach dem Verlassen des Gültigkeitsbereiches von x wiederum aufgehoben und x’ erhält ihren ursprünglichen Wert. ; Definition einer Variable x , als x ’ bezeichnet ( defvar x 1 ) ; D e f i n i t i o n d e r Funktion foo , w e l c h e den Wert von x z u r ü c k g i b t ( defun f o o ( ) x) ; Funktionsrumph ; Definition einer Variable x ( let (( x 2)) ; e r s t e r A u f r u f von f o o ( f o o ) ) ; −> 2 ; z w e i t e r A u f r u f von f o o ( foo ) ; −> 1 Listing 8: Beispiel für dynamische Bindung Listing 8 imitiert das oben genannte Szenario mithilfe der dynamischen Variablenbindung. Im Gegensatz zum vorigen Beispiel existieren hier zum Zeitpunkt des ersten Aufrufs von foo keine zwei Variablen, die sich gegenseitig überdecken, sondern genau eine: die Variable x’, welche im Gültigkeitsbereich von x den Wert von x annimmt. Obwohl sich der Funktionsrumpf von foo nicht im Gültigkeitsbereich von x befindet, wird trotzdem der Wert von x zurückgegeben. Bevor der zweite Aufruf der Funktion foo stattfindet, wird der Gültigkeitsbereich von x verlassen, womit x’ den ursprünglichen Wert erhält und der zweite Auruf von foo somit ein anderes Ergebnis liefert. Das Konzept der dynamischen Variablenbindung ist vor allem dann vorteilhaft, wenn die Laufzeitumgebung temporär angepasst werden muss. Definiert man sich beispielsweise eine globale, dynamisch gebundene Variable Standardoutput, so lassen sich jederzeit auf einfache Weise bestimmte Ausgaben temporär umleiten. Um unbeabsichtigte Änderungen an globalen, dynamisch gebundenen Variablen durch Einführung neuer gleichnamiger Variablen zu vermeiden, wurde in diesem Zusammenhang eine Namenskonvention eingeführt, welche besagt, dass Namen globaler Variablen von zwei * (Asterisk) - Symbolen eingeschlossen werden sollen. So sollte eine globale Variable nicht abc heißen, sondern eben 7 *abc*. Auch wenn die Nutzung dieses Konzepts an manchen Stellen vorteilhaft und einfach erscheinen mag, so steigt mit zunehmender Komplexität des Programms bzw. bei Multithreading natürlich die Wahrscheinlichkeit des Auftretens eines unbeabsichtigter Nebeneffekts, da womöglich gar nicht mehr ersichtlich ist, welche Teile des Programms bzw. welche Threads durch dynamisch gebundene Variablen und deren Änderungen beeinflusst werden. 2.3 CLOS, Method Dispatching und MOP Das Common Lisp Object System, kurz CLOS, ist eine im ANSI-Standard für CL spezifizierte, objektorientierte Erweiterung für Common Lisp. Es führt das Klassenkonzept und das Konzept der Mehrfachvererbung in Common Lisp ein, zudem erlaubt es die Definition von generischen Funktionen. 2.3.1 CLOS - Common Lisp Object System Listing 9 zeigt die Definition dreier einfacher Klassen mithilfe des defclass Makros. ( d e f c l a s s shape ( ) ( ) ) ( d e f c l a s s r e c t a n g l e ( shape ) ( ( height : i n i t f o r m 0) ( width : i n i t f o r m 0 ) ) ) ( d e f c l a s s c i r c l e ( shape ) (( radius : initform 0))) Listing 9: Definition einfacher Klassen Der erste Parameter ist der Name der Klasse, gefolgt von einer Liste von Basisklassen. Nächster Parameter ist eine Liste von Slotspezifikationen, wobei Slots vergleichbar mit Membervariablen in Java sind. So eine Slotspezifikation muss zumindest aus dem Namen des Feldes bestehen, wahlweise mit Optionen wie z.B. dem Initialwert versehen. Da eine Klasse in Common Lisp von mehreren Basisklassen ableiten kann, muss bei einem Namenskonflikt geerbter Slots eine Priorisierung der Basisklassen vorgenommen werden. Dazu bestimmt Common Lisp anhand der Reihenfolge bei der Angabe der Basisklassen eine Ordnung, wobei die erste der Liste jene mit der höchsten Priorität ist. Diese Rangordnung wird auch als Präzedenzliste der Klasse bezeichnet. Nach der Definition einer Klasse können beliebig viele Instanzen dieser mithilfe der Funktion make-instance erzeugt werden (Siehe Listing 10). 8 ( defvar my−rec ( make−instance ’ r e c t a n g l e ) ) Listing 10: Erzeugen einer Instanz 2.3.2 Generische Funktionen und Method Dispatching Anders als viele andere objektorientierte Sprachen, wie z.B. Java, verfolgt Common Lisp nicht das Konzept, Methoden mit Klassen zu assoziieren. In Sprachen wie Java werden Instanzmethoden auf bestimmten Objekten ausgeführt, wobei die Klasse des Objekts den auszuführenden Code bestimmt. Diese Art des Methodenaufrufs wird auch als message passing bezeichnet. Common Lisp verfolgt einen gänzlich anderen Ansatz, Methoden zu gruppieren: mit generischen Funktionen. Eine generische Funktion ist eine abstrakte Operation, welche zwar den Namen und die Parameterliste definiert, jedoch deren Implementierung außen vor lässt (Siehe Listing 11). ( defgeneric a r e a ( any−shape ) ) Listing 11: Definition einer generischen Funktion Zu einer generischen Funktion können nun mehrere Methoden implementiert werden, wobei deren Name und Parameterliste übereinstimmen müssen, die Parameter zudem jedoch auch durch Angaben von Typen spezialisiert werden können (Listing 12). Existiert zu einer angegebenen Methode keine generische Funktion, so wird diese implizit erzeugt. ( defmethod a r e a : b e f o r e ( ( s shape ) ) ( print ” Area o f shape : ” ) ) ( defmethod a r e a ( ( c c i r c l e ) ) ( print ( ∗ 3 . 1 4 1 5 9 ( s q u a r e ( slot−value c ’ r a d i u s ) ) ) ) ) ( defmethod a r e a ( ( r r e c t a n g l e ) ) ( print ( ∗ ( slot−value r ’ h e i g h t ) ( slot−value r ’ width ) ) ) ) ; A u f r u f d e r g e n e r i s c h e n Funktion ( a r e a my−rec ) ; −> ”Area o f sh a pe : ” 25 Listing 12: Methoden als Implementierungen einer generischen Funktion Wird nun eine generische Funktion aufgerufen, erstellt diese erst eine Liste all ihrer für die angegebenen Argumente ausführbaren Methoden. Diese Methodenliste wird anschließend anhand derer Spezialisierungen, also Typangaben, sortiert, wobei die am besten passendste Methode an die erste Stelle gereiht wird. Daraufhin ruft die generische Funktion die erste Methode der Liste auf, 9 welche wiederum mithilfe der Funktion call-next-method die nächste Methode der Liste aufrufen kann (aber nicht muss), usw. Dieses Verfahren wird als method dispatching bezeichnet. Es ist der Kern des Konzepts der generischen Funktionen in Common Lisp und stellt gleichzeitig den Hauptunterschied zu message passing Systemen (wie z.B. Java) dar. Eine Methode kann zusätzlich mit einem sog. method qualifier (:before, :after und :around ) versehen werden, der die Reihenfolge der ausführbaren Methoden entscheident beeinflusst. 2.3.3 MOP - Metaobject Protocol In Common Lisp ist eine Klasse ebenso ein Objekt, das sog. Metaobject, über welches Informationen über die Klasse, wie z.B. die direkten Basisklassen, abgerufen werden bzw. bestimmte Eigenschaften der Klasse modifiziert werden können. Die Metaobjects sind wiederum Instanzen von Metaklassen, welche auch wiederum gleichzeitig Objekte darstellen, usw. Das Verhalten und die Möglichkeiten der Metaobjects werden als Metaobject Protocol bezeichnet. Das Common Lisp Metaobject Protocol ist im Gegensatz zur Sprache selbst und dem CLOS nicht standardisiert, jedoch gilt die Spezifizierung aus dem Buch The Art of Metaobject Protocol von Gregor Kiczales, Jim des Rivières, und Daniel G. Bobrow als DIE Richtlinie zur Implementierung eines MOP. Zu jedem der Objekte, die im vorgehenden Abschnitt eingeführt wurden, gibt es entsprechende Metaobjects, so dass diese vom Programmierer angepasst werden können. Mithilfe dieser Metaobjects können grundsätzlich alle Eigenschaften, die bei der Definition der verschiedenen Objekte angegeben werden, im nachhinein verändert werden. Klassen Ein Klassen - Metaobject kann beispielsweise dazu verwendet werden, die Präzedenzliste (Auswahlliste bei Namenskonflikten) anzupassen oder direkte Super- oder Subklassen abzufragen und zu ändern. Normale Klassen sind selbst Instanzen der Klasse standard-class Slots Nützlich, um beispielsweise den Namen oder den Initialwert eines Slots abzufragen/anzupassen. Instanzen von standard-slot-definition repräsentieren Slots. Methoden Mithilfe von Methoden - Metaobjects können Informationen über die Parameter und die Spezifizierer (Argumenttypen) abgefragt/angepasst werden. Normale CLOS-Methoden sind Instanzen der Klasse standard-method. 10 Desweiteren existieren analoge Metaobjects zu generischen Funktionen, Spezialisierer und den nicht erwähnten Methodenkombinationen. ; Alle Basisklassen ( class−direct−superclasses ( find−class ’ c i r c l e ) ) ( a r e a my−rec ) ; −> ”Area o f sh a pe : ” 25 ; Ändert den Method Q u a l i f i e r : b e f o r e ( s e t f ( f i r s t ( method−qualifiers ( find−method #’ a r e a ( l i s t ’ : b e f o r e ) ( l i s t ( find−class ’ shape ) ) ) ) ) ’ : a f t e r ) ( a r e a my−rec ) ; −> 25 ”Area o f s ha p e : ” Listing 13: Beispiele zum Umgang mit Metaobjects Mithilfe des MOP kann das Common Lisp Object System (auch zur Laufzeit) entscheidend beeinflusst werden. Es ermöglicht das Manipulieren der Vererbungsmechanismen (also was wird wovon abgeleitet) und des Konzeptes des Method Dispatching (welche Methoden werden in welcher Reihenfolge ausgewertet). Dies betrifft nicht nur selbst definierte Klassen und generische Methoden, sondern auch aus fremden Modulen importierte Klassen und Methoden. Ein anderes Beispiel für die Mächtigkeit des Metaobject Protocols ist das Persistieren (haltbar machen) von Klassenobjekten: Das MOP ist in der Lage, bestimmte Metaobjekte (also Klassen, generische Funktionen, usw.) so anzupassen, dass mit jeder Instanzierung eines Klassenobjekts ein Eintrag in einem File getätigt wird und in diesem der Status inklusive Slotwerte abgelegt wird. Dazu wird ganz einfach das Standardverhalten bestimmter Funktionen und Methoden durch benutzerdefiniertes Verhalten ersetzt, wenn nötig, auch zur Laufzeit. Zusammen mit dem Makrosystem ist das Metaobject Protocol hauptverantwortlich dafür, dass Common Lisp eine reflexive Programmiersprache ist. Dieses Paradigma ist in Common Lisp auch bei weitem stärker ausgeprägt ist als beispielsweise in Java, wo Reflection durch spezielle APIs eingeführt werden kann und somit nur auf die Sprache “aufgesetzt” wird. Der Einsatz von Reflection führt in Java aber nicht selten zu Performance - und Sicherheitsproblemen, wohingegen das MOP so eng mit der Sprache selbst verknüpft ist, dass Common Lisp bei der Verwendung des MOP keineswegs unter Performance-Einbußen leidet. 3 Evaluierung/Zusammenfassung Die Sprache Common Lisp wird wegen ihrer Anpassbarkeit und Erweiterbarkeit nicht umsonst als die “programmierbare Programmiersprache” bezeichnet. Das 11 überaus mächtige Makrosystem der Sprache ist nicht mit jenem der Sprache C zu vergleichen, sondern viel mehr als Werkzeug zur Manipulation der Sprache und deren Syntax zu verstehen. Transformationen der abstrakten Syntaxbäume sind so einfach zu bewerkstelligen wie Definitionen neuer Funktionen, wie auch das erwähnte Beispiel mit der Infix-Präfix - Transformation zeigt. Weiters bietet Common Lisp mithilfe der dynamischen Variablenbindung einfache Möglichkeiten, die Laufzeitumgebung für einen bestimmten Zeitraum anzupassen. Umleitung von Ausgaben und Eingaben sind Paradebeispiele dafür. Jedoch hat dieses Konzept durchaus seine Tücken, die vor allem bei steigender Komplexität des Programms oder beim Einsatz von Multithreading zum Vorschein kommen. Das Common Lisp Object System führt in Common Lisp das Klassenkonzept ein, jedoch in einer etwas anderen Art als es aus Java bekannt ist. In Common Lisp werden Methoden, im Gegensatz zu Java, mithilfe von generischen Funktionen gruppiert. Diese generischen Funktionen bestimmen dann bei ihrem Aufruf, welche ihrer Methoden in welcher Reihenfolge ausgeführt werden sollen. Dieses Konzept des Method Dispatching ist also sozusagen der Gegensatz zum Message Passing - Konzept aus Java, wo ja die Klasse eine auszuführende Methode bestimmt. Zudem ist das Verhalten von Klassen, Methoden und anderen Common Lisp Objekten zur Laufzeit durch das Metaobject Protocol abfragbar und manipulierbar. Von der Präzedenzliste von Klassen bis hin zur Reihenfolge der ausführbaren Methoden einer generischen Funktion: alles kann an die eigenen Bedürfnisse angepasst werden, sodass das MOP (und das Makrosystem) eine einfache Implementierung von domain specific languages ermöglicht. Alles in allem ist Common Lisp zwar eine im ANSI Standard spezifizierte Sprache, was allerdings nicht heißt, dass sich der Programmierer damit zufrieden geben muss. Es ist zwar nicht zu erwarten, dass die Sprache Common Lisp sich in Zukunft über steigende Beliebtheit erfreuen darf, einige Konzepte daraus dürften aber sehr wohl in andere, beliebtere Sprachen einfließen bzw. als Vorbild dienen. Literatur [1] Jim des Rivières Daniel G. Bobrow Gregor Kiczales. The Art of the Metaobject Protocol. MIT Pr, 1991. [2] Christian Queinnec. Lisp in Small Pieces. Cambridge University Press, 1996. [3] Peter Seibel. Practical Common Lisp. Apress, 2005. 12