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