Donnerstag, 4. Dezember 2008

entwickler.com Magazine Konferenzen Entwickler Akademie Entwickler-Forum Jobbörse Bücher
Software & Support Verlag




April 2006
Alberne Klammern?
Lisp - Die programmierbare Programmiersprache
von Armin Roehrl und Stefan Schmiedl

Pascal is for building pyramids - imposing, breathtaking structures built by armies pushing heavy blocks into place. Lisp is for building organisms ... - Alan Perlis

Die Programmiersprache Lisp hat die letzten 40 Jahre dynamisch gemeistert und darf als ausgereift bezeichnet werden. Lisp-Code kann sowohl interpretiert als auch kompiliert werden, was neben einer bequemen, inkrementellen Entwicklung auch eine vernünftige Ausführungsgeschwindigkeit ermöglicht. Lisp ist sehr viel weiter verbreitet als man denkt: Vom Finanzbereich bis zum Telekommunikationsanbieter findet man Erfolgsstorys von Lisp [Erfolg]. Die bekannteste dürfte wohl der Artikel des Lisp-Gurus Paul Graham [Viaweb] sein, der seinen (in Lisp programmierten) eCommerce Store Viaweb für $50 Millionen an Yahoo verkaufen konnte.


Wenn man als Programmierer nicht einrosten will, sollte man jedes Jahr eine neue Sprache lernen. Wenn's 2000 Java war und 2001 Ruby, warum nicht für 2002 Lisp vorsehen? Man wird viele bekannte Sprachkonstruktionen wiederentdecken, die in Lisp schon jahr(zehnt)elang gang und gäbe sind.

Lisp hilft Programmierzeit zu senken.
Die NASA gab verschiedenen Teams die gleiche Programmieraufgabe in C, C++, Java und Lisp. Es wurden die Programmierzeit, der Speicherbedarf der Programme und die Geschwindigkeit gemessen. Das interessante Ergebnis der Studie [NASA] besagt: Lisps Geschwindigkeit war mit der Geschwindigkeit von C und C++ vergleichbar. Der Speicherbedarf war in etwa wie der von Java, aber die Lisp-Programmierteams hatten mit Abstand als erste die Aufgabe gelöst. In die gleiche Richtung geht der bekannte Artikel [Hindsight]: Mit Accelerating Hindsight: Lisp as a Vehicle for Rapid Prototyping betitelt zeigt er eindrucksvoll die Stärke von Lisp für Rapid Prototyping und wieso das etwas Gutes ist. So manches Projekt hinterlässt einen schlechten Nachgeschmack: Oft weiß man erst, wenn ein Projekt zu Ende ist, wie man es hätte machen sollen. Manager wollen so schnell wie möglich ein richtiges Produkt und keinen Prototypen, aber diese Sichtweise kann sich schnell als nur kurzfristig tauglich herausstellen. Lisp erlaubt es, in sehr kurzer Zeit etwas zu schaffen, das man beurteilen kann, zum Beispiel die Durchführbarkeit eines neuen Ansatzes. Damit bleibt mehr Zeit für die Implementation der Lösung, die sich in dieser Probephase als richtig erwiesen (!) hat. Die vier Hauptpunkte, wieso Lisp gut für Rapid Prototyping ist, sind:
  • Lisp ist reichhaltig
  • Lisp verfügt über generische Funktionen und Message Passing
  • Lisp unterstützt modulares Design (unter anderem mit einem Condition-System, Macros, optionalen Deklarationen, Funktionen höherer Ordnung)
  • Lisp ermöglicht eine interaktive Entwicklung und Debugging in einer einheitlichen Umgebung
[Hindsight] untermauert diese Aussagen durch ausführliche Codebeispiele, sollte jedoch erst gelesen werden, nachdem man Grundkenntnisse in der Syntax von Lisp besitzt. Kent M. Pitman, ein bekannter Lisp-Guru, wurde auf Slashdot gefragt, was für ihn das wichtigste an einer abstrakten Sprache sei? Er antwortete darauf: Meine Zeit, da sie nicht erneuerbar ist. Am Ende seines Lebens wolle er sagen können, dass er viele interessante Dinge getan hat und nicht nur ein paar interessante, die aber dafür wirklich schnell gelaufen sind [Pitman]. Lisp (= LISt Processor, nicht Lots of Insignificant and Silly Parentheses) wurde Ende der 50er Jahre von McCarthy entwickelt und 1959 gab es die LISP1 Implementation [LispGeschichte1, LispGeschichte2]. Lisp ist damit ein bisschen jünger als Fortran (1954-57) [FortranGeschichte] und doch erstaunlich moderner. Lisp beruht auf der Theorie des Lambda-Kalküls und wurde von Anfang an mit dem Ziel entwickelt, die Möglichkeit zu vollständiger Abstraktion zu liefern. Dies ist sicher mit ein Grund, wieso vor allem AI-Forscher Lisp als ihre Sprache zu schätzen gelernt haben. Diese Reinheit ging in der Vergangenheit auf Kosten der Geschwindigkeit, doch mittlerweile ist Lisp (wegen optimierender Compiler) schnell geworden. Anfang der 70er Jahre haben Forscher zwei neue Hardware-Techniken eingesetzt, um Lisp zu beschleunigen: Tagged Architecture und Micro-Programming. Tagged Architecture ermöglicht den einfachen Umgang mit Daten, deren Typ sich dynamisch ändern kann. Mit Micro-Programming kann man auf Hardware-Ebene komplexe Instruktionen erstellen, die für die high-level Lisp Semantik vorteilhaft sind. Diese Experimente führten letztendlich zu speziellen Lisp Computern, so genannten Lisp Maschinen [LispMachine], die sich aber vor allem auf Grund des hohen Preises nie wirklich durchsetzen konnten. Symbolics Inc. [Symbolics] stellte eine komplette Workstation her, inklusive Entwicklungsumgebung Genera, ebenso hatten Xerox und Texas Instruments eigene Produkte. Die zahlreichen Lisp-Dialekte wurden in den 80er Jahren zu Common Lisp standardisiert. Das Ergebnis ist Common LISP, dessen Spezifikation [hyperspec] auch online verfügbar ist. Es gibt verschiedene Varianten und Abkömmlinge von Lisp: die bekanntesten dürften Scheme und T sein. In letzter Zeit macht Dylan immer wieder von sich reden, eine moderne Sprache, die viel von Lisp geerbt hat, nur nicht die einfache Syntax. Zwei der bekanntesten Lisp Anwendungen dürften wohl Emacs [Elisp] und AutoCAD sein, die beide einen Lisp-Dialekt zur Anpassung und Automatisierung einsetzen.

CLISP Installation
Wir haben als Software für diesen Artikel die Open Source Implementation CLisp gewählt. Die zwei bekanntesten kommerziellen Lisp-Implementationen dürften von Xanalys (von Harlequin übernommen) [Xanalys] und Franz [Franz] kommen. Beide bieten auch eine kostenlose Testversion an. Clisp wird am einfachsten per rpm -ih clisp.rpm installiert. Die meisten Linux-Distributionen, wie z.B. Suse liefern automatisch clisp mit. Oder man kompiliert den Sourcecode: 8-10MB Sourcecode herunterladen [CLISP], entpacken, konfigurieren und kompilieren:

tar xzvf clisp-2.27.tar.gz
cd clisp-2.27
./configure --with-module=clx/new-clx --with-module=regexp
cd src
./makemake --with-module=clx/new-clx --with-module=regexp --with-readline --with-gettext --with-dynamic-ffi > Makefile
make config.lisp
vi config.lisp
make
make check

In der Datei config.lisp sollte man den Namen des Rechners eintragen und gegebenenfalls den URL auf eine lokale Kopie der Common Lisp HyperSpec [hyperspec] setzen.

...
(defun short-site-name () "workhorse")
(defun long-site-name () "workhorse.approximity.com")
...
(setq *clhs-root-default* "file:///usr/local/share/doc/HyperSpec/")

Diese Adresse wird verwendet, um von CLisp aus Definitionen nachzuschlagen:

[1]> (setq *browser* :lynx)
:LYNX
[2]> (clhs "setq")

Man kann Module wie GNU-Regexp für reguläre Ausdrücke oder CLX für ein Xlib-Äquivalent dazu linken. Dies kann bei Bedarf auch nachträglich geschehen, indem man die neue Bibliothek kompiliert und dann mit dem übrigen System zu einem neuen Linkset kombiniert. Beim Aufruf von CLisp kann dann das entsprechende Linkset (base und full sind normalerweise vorhanden) ausgewählt werden:

> clisp -K <linkset>

Der read-eval-print-loop
Die Interaktion mit einer Lisp-Umgebung geschieht eigentlich immer in dieser klassischen Schleife: Ein LISP-Interpreter liest einen Lisp-Ausdruck, wertet ihn aus, und zeigt das Ergebnis an. Ein Lisp-Ausdruck wird entweder als Funktionsaufruf (mit Klammern) oder als symbolischer Name für einen Wert (ohne Klammern) interpretiert. Namen können prinzipiell jedes beliebige Zeichen enthalten, in der Regel wird zwischen Groß- und Kleinschreibung nicht unterschieden. Mehrteilige Namen werden durch einen Bindestrich gegliedert.

[1]> (lisp-implementation-version)
"2.27 (released 2001-07-17) (built 3215460179) (memory 3215461181)"
[2]> (+ 1 2 (* 2 2))
7
[3]> *print-case*
:UPCASE

Durch die Präfix-Notation entfallen implizite Regeln wie Punkt vor Strich oder der Vorrang verschiedener Operatoren, wodurch Lisp-Programme nach einer kurzen Eingewöhnung sehr gut lesbar werden. Durch die Klammern ist auch sofort klar, wie viele Argumente zu einem bestimmten Funktionsaufruf gehören. Mit einem guten Editor (emacs und vi hatten eigentlich schon immer einen Lisp-Modus), der sich um die Einrückung kümmert und die Bezugsklammer anzeigen kann, ignoriert man nach kurzer Zeit Häufungen schließender Klammern.

Schnupperkurs
Für einen tieferen Einstieg in Lisp empfehlen wir eine der Lisp-Bibeln [Slade, Graham]. Angenehm fällt bei den beiden auf, dass es keine AI-Bücher sind. Wer in AI mit Lisp einsteigen will, sollte [Norvig, Winston] lesen. Eines der besten Nachschlagewerke mit viel Informationen ist nach wie vor [Steele], das auch komplett online [SteeleOnline] verfügbar ist.

Obligatorisches Hello, World
Listing 1



Listing 1 zeigt ein Hello World-Programm. Als Minimalversion würde auch nur (princ "Hello, world!") ausreichen. Die readline-Unterstützung von CLisp beschränkt sich übrigens nicht nur auf die Möglichkeit, die Eingaben per Cursortasten zu editieren, zum Beispiel kann man mit der Tab-Taste Funktions- und Variablennamen ergänzen lassen.

[1] > *print-<Tab>
*print-array* *print-gensym* *print-radix*
*print-base* *print-indent-lists* *print-readably*
*print-case* *print-length* *print-right-margin*
*print-circle* *print-level* *print-rpars*
*print-closure* *print-pathnames-ansi* *print-symbols-long*
*print-escape* *print-pretty*

Atome und Listen
Ein Lisp-Atom ist ein symbolischer Name oder eine Zahl, Listen setzen sich aus Atomen oder Listen zusammen. Das Atom nil und die leere Liste () sind äquivalent, ansonsten gibt es keine Überschneidungen. Die erste Aufgabe des Evaluators "eval" ist es, den Wert eines Atoms zu finden.

[1] *print-base*
10

Die Sternchen am Anfang und am Ende kommen daher, dass globale Werte traditionell so markiert werden. Wenn eval auf eine Liste stößt, wird angenommen, dass der erste Eintrag der Name einer Funktion ist, die übrigen Elemente die Argumente für den Aufruf sind, die jeweils für sich ausgewertet werden, bevor die Funktion selbst ausgeführt wird. (Name-der-Funktion 1.-Argument 2.-Argument ...)

[2] (+ 8 8)
16
[3] (setq *print-base* (+ 8 8))
10
[4] (+ 8 8)
10

Neben Funktionen (wie +) gibt es noch special forms, die die Auswertung ihrer Argumente kontrollieren können: setq zum Beispiel nimmt den Variablennamen direkt und wertet nur den zweiten Ausdruck aus.

Listen manipulieren
Eine Liste setzt sich aus zweiteiligen Zellen zusammen: der erste Teil (car) enthält den eigentlichen Wert und der zweite Teil (cdr) enthält den Rest:

[1]> (setq liste (list 1 2 3 4 5))
(1 2 3 4 5)
[2]> (car liste)
1
[3]> (cdr liste)
(2 3 4 5)
[4]> (second liste)
2
[5]> (elt liste 1)
2

An Stelle der historisch bedingten Namen car und cdr kann man auch first und rest verwenden.

Definieren von Funktionen
Funktionen in Lisp haben *sehr* flexible Parameterlisten. Neben notwendigen Parametern können noch optionale Parameter und Schlüsselwort-Parameter, letztere mit Standardwerten, angegeben werden. Schließlich gibt es noch die Möglichkeit, alle noch nicht anderweitig verwendeten Argumente über einen rest-Parameter an die Funktion zu übergeben.

Definieren von Funktionen
Funktionen in Lisp haben *sehr* flexible Parameterlisten. Neben notwendigen Parametern können noch optionale Parameter und Schlüsselwort-Parameter, letztere mit Standardwerten, angegeben werden. Schließlich gibt es noch die Möglichkeit, alle noch nicht anderweitig verwendeten Argumente über einen rest-Parameter an die Funktion zu übergeben.

Listing 2

[1]> (defun square (x) (* x x))
SQUARE
[2]> (square 9)
81
[3]> (defun hoch (x optional (y 2)) (expt x y))
HOCH
[4]> (hoch 3 4)
81
[5]> (hoch 3)
9
[6]> (defun potenziere (x key (hoch nil hoch-p))
(if hoch-p
(expt x hoch)
(error "Exponent fehlt")))
POTENZIERE
[7]> (potenziere 2)
*** - Exponent fehlt
1. Break [8]> :bt1
...
1. Break [9]> :h
...
1. Break [10]> :a

Im Beispiel in Listing 2 wird durch die Fehlermeldung im aktuellen read-eval-print-loop ein so genannter break-loop gestartet, der einige (implementationsabhängige) Besonderheiten aufweist. In einer integrierten Entwicklungsumgebung könnte man jetzt (komfortabel) einen Programmierfehler beseitigen und im Anschluss das Programm an der gleichen Stelle weiter ausführen lassen.

Verzweigungen
Neben dem bekannten (if test-expr true-expr false-expr) und dem Gegenstück (unless test-expr false-expr true-expr) ist die klassische Verzweigung in Lisp die cond-Anweisung, die einem hochgezüchteten case/switch-Statement gleicht.

(cond (test-a epxr-a1 expr-a2 ... result-a)
(test-b expr-b1 expr-b2 ... result-b)
...
(t expr-z1 expr-z2 ... result-z))

Die Tests werden der Reihe nach ausgewertet, der erste Zweig, dessen Test wahr (t) ist, wird ausgewertet. Das besondere ist, dass die Tests für die einzelnen Zweige voneinander unabhängig sein können, sie müssen sich nicht darauf beschränken, verschiedene Werte einer Variablen abzuprüfen.

Rekursive Funktionen
Typisch Lisp sind rekursive Strukturen, die oft elegante Formulierungen ermöglichen (siehe Listing 3).

Listing 3

[1]> (defun fak (n)
(if (zerop n) 1
(* n (fak (1- n)))))
FAK
[2]> (fak 5)
120
[3]> (trace fak)
;; Tracing function FAK.
(FAK)
[4]> (fak 5)

1. Trace: (FAK '5)
2. Trace: (FAK '4)
3. Trace: (FAK '3)
4. Trace: (FAK '2)
5. Trace: (FAK '1)
6. Trace: (FAK '0)
6. Trace: FAK ==> 1
5. Trace: FAK ==> 1
4. Trace: FAK ==> 2
3. Trace: FAK ==> 6
2. Trace: FAK ==> 24
1. Trace: FAK ==> 120
120

Schleifen und Iteratoren

[1]> (dotimes (i 3 "fertig") (print i))

0
1
2
"fertig"
[2]> (dolist (i '(a b c) "fertig") (print i))

A
B
C
"fertig"

Neben diesen konventionellen Schleifen gibt es noch die allgemeine do-Schleife, bei der zuerst lokale Variablen definiert werden, die nur in der Schleife verfügbar sind. Die Bedingung muss erfüllt sein, damit die Schleife abbricht:

[3]> (do ((i 10 (1+ i))
(j 2 (+ j 2)))
((> j i) "überholt")
(format t "~2d - ~2d~" i j))
10 - 2
11 - 4
12 - 6
13 - 8
14 - 10
15 - 12
16 - 14
17 - 16
18 - 18
"überholt"

Schließlich stellt Lisp noch eine Reihe von Iteratoren zur Verfügung, die einen effizienten Umgang mit Listen erlauben.

[1]> (mapcar #'print '(a 1 b 2))

A
1
B
2
(A 1 B 2)

CLOS
Das Common Lisp Object System ist eine sehr mächtige Lisp-Erweiterung, die auf echten generischen Funktionen basiert und überaus flexibel ist, wenn es um die Kombination von Methoden geht (siehe Listing 4).

Listing 4

[1]> (defclass punkt ()
((x :accessor x :initform 0 :initarg :x)
(y :accessor y :initform 0 :initarg :y)))
#<STANDARD-CLASS PUNKT>
[2]>
(defun make-punkt (x y)
(make-instance 'punkt :x x :y y))
MAKE-PUNKT
[3]>
(setq a (make-punkt 10 20))
#<PUNKT #x20373C61>
[4]> (setf (x a) 15)
15
[5]> (x a)
15
[6]> (defmethod translate ((a punkt) (b punkt))
(incf (x a) (x b))
(incf (y a) (y b))
a)
#<STANDARD-METHOD (#<STANDARD-CLASS PUNKT> #<STANDARD-CLASS PUNKT>)>
[7]> (translate a (make-punkt 4 8))
#<PUNKT #x20373C61>
[8]> (defmethod print-object ((p punkt) stream)
(format stream "#<(~f/~f)>" (x p) (y p)))
#<STANDARD-METHOD (#<STANDARD-CLASS PUNKT> #<BUILT-IN-CLASS T>)>
[9]> a
#<(19.0/28.0)>

An den Ergebnissen von Schritt 3 und 7 kann man sehen, dass es sich wirklich um das gleiche Objekt handelt, mit der print-object-Methode wird eine lesbare Ausgabe erzeugt. Die Ausdrücke mit setf und incf ändern den Wert der mit ihrem ersten Argument bezeichnet wird. Beim Parametertypen muss es sich nicht unbedingt um eine selbst definierte Klasse handeln. Man kann mit Typbezeichnern wie unter anderem mit NUMBER, FLOAT, INTEGER, RATIONAL, REAL oder COMPLEX die eingebauten Lisp-Typen referenzieren. Kurz gesagt, wird diejenige Methode mit den speziellsten passenden Typen ausgewählt.

Alle haben von Lisp das beste kopiert, wieso dann noch Lisp lernen?
Kent M. Pitman argumentiert, dass Sprachfeatures ein ökologisches System bilden, sodass es nicht ausreicht, Sprachen Feature für Feature zu vergleichen. Wesentlich ist die Integration der Elemente in der Sprache. Um Lisp wirklich einschätzen zu können, muss man es eine Zeit lang benützt haben. Hier sind ein paar Pluspunkte, die Pitman [Pitman1, Pitman2] auf Slashdot aufzählte:
  • Lisp ist dynamisch. Sogar in einem laufenden Image können in einem Debugger Funktionen, Klassen usw. geändert und neu definiert werden. Der laufende Code benützt ab sofort die neuen Methoden. Dies geht sogar, wenn die neue Klasse andere Slots hat, und man kann definieren, wie das Update von den alten auf die neuen für bereits existierende Instanzen gehen soll.
  • Lisp ist introspektiv. * Die Syntax von Lisp ist sehr formbar. Lisp erlaubt es Programmierern, die Syntax beliebig umzudefinieren. Nicht umsonst kommt der Name Emacs von Editing macros.
  • Lisp erzwingt keine Deklarationen von Variablentypen, wodurch Prototyping schneller wird. Andererseits können Deklarationen in funktionierenden Code eingebettet werden, um die Ausführungsgeschwindigkeit oder den Speicherbedarf zu optimieren.
  • Lisp besitzt ein mächtiges Klassensystem und ein flexibles Meta-Klassen System. Das Klassensystem ermöglicht mächtige Slot- und Methodendefinitionen. Das Meta-Klassen System erlaubt es dem Benutzer, das Objektsystem wie Daten zu behandeln, die programmiert werden können, um neue Klassen zu erstellen.
  • automatische Speicherverwaltung.
  • mächtige integrierte Tools.
Letzter Tip für Programmierer die unter Java-wütigem Management leiden: Mittels [Jscheme] oder [Jfranz] kann man Lisp durch die Hintertür in Java-Projekte mogeln. [Jfranz] erlaubt den Aufruf von Java-Klassen und [Jscheme] erlaubt die Einbindung von Scheme in Java-Programme.

Links

Tote Bäume
  • [Graham] ANSI Common LISP, Paul Graham, Prentice Hall, 1995
  • [Norvig] Paradigms of Artificial Intelligence Programming, Morgan Kaufmann, 1991
  • [Winston] Lisp, Patrick Henry Winston, Berthold Klaus Paul Horn, Addison Wesley, 1988
  • [Slade] Object-Oriented Common Lisp, Stephen Slade, Prentice Hall 1997
  • [Keene] Object-Oriented Programming in Common Lisp: A Programmer's Guide to CLOS, Addison Wesley, 1988
  • [Steele] Common Lisp: The Language, Guy L. Steele Jr., 2. Auflage, Digital Press, 1990

Common Lisp Überblick

Common Lisp ist
  • interaktiv
  • ein Lisp für den professionellen Gebrauch
  • für konventionelle und AI-Projekte geeignet
Common Lisp Programme sind
  • einfach zu testen (interaktiv)
  • leicht zu maintainen
  • portabel (Es gibt einen Standard für die Sprache und die Bibliotheksfunktionen)
CLisp
  • braucht nur 2MB Speicher)
  • implementiert fast den gesamten ANSI Standard, sowie einige Erweiterungen)
  • arbeitet mit vi und emacs)
  • ist kostenlos)
Common Lisp bietet
  • klare Syntax und Semantik
  • reichhaltige Datentypen: Zahlen, Strings, Arrays, Listen, Characters, Symbols, Structures, Streams, usw.
  • runtime typing: Der Programmierer braucht keinen Typendeklarieren, aber er wird benachrichtigt, wenn es Probleme gibt
  • viele generische Funktionen: 88 arithmetische Funktionen für alle Art von Zahlen (ganze, rationale, fließkomma- und komplexe Zahlen), 44 Such-, Filter- und Sortier-Funktionen für Listen, Arrays und Strings
  • automatische Speicherverwaltung (garbage collection)
  • Programme sind in Module packbar
  • ein Objekt System, generische Funktionen mit mächtigen Methodenkombinationen
  • Makros: Jeder Programmierer kann seine eigenen Spracherweiterungen machen
CLISP bietet
  • einen Interpreter
  • einen Compiler, der 5-mal so schnell wie der Interpreter ist
  • einen Source-level Debugger, der es erlaubt durch interpretierten Code zu steppen
  • alle Datentypen können beliebig groß sein (die Größe wird nie deklariert und kann dynamisch verändert werden)
  • beliebig lange Integer
  • Beliebig-genaue Arithmetik bei floating point Operationen
  • Mehr als 800 Bibliotheksfunktionen und Makros, mehr als 600 davon sind in C geschrieben


    Hat Ihnen dieser Artikel gefallen? Dann abonnieren Sie das Entwickler Magazin direkt über unser Online-Formular.

zur vorherigen Seite
zurück
an den Anfang der Seite
nach oben
Diesen Artikel drucken
drucken
Diesen Artikel weiterempfehlen
empfehlen

Software & Support Verlag GmbH