Systemtechnische Esoterik leicht gemacht

Lispeln lohnt sich!
Kommentare

Der durchschnittliche Programmierer kennt Paul Graham wegen seines Postulats, wonach junge Informatiker in ihrer Schulzeit ein besonders schweres Leben hätten. Weniger bekannt ist, dass der Amerikaner den Erfolg seines Start-ups Viaweb auf eine alte Programmiersprache namens Lisp zurückführt. Warum sich ein Blick auf Lisp lohnt, erklärt dieser Beitrag.

Laut Graham war sein Start-up unter anderem deswegen erfolgreich, weil die hauseigenen, effizienten Lisp-Programmierer zum Realisieren derselben Funktion weniger Zeit benötigten. Sein Unternehmen Viaweb konnte auf diese Art und Weise die von der Konkurrenz neu auf den Markt gebrachten Funktionen binnen kürzester Zeit nachbilden und so die Featureparität bewahren. Wer heute den Begriff Lisp hört, denkt meist an hoch finanzierte AI-Forschungsprojekte der US-Regierung. Das ist mit Sicherheit nicht falsch, da sich die Sprache im Laufe der letzten Jahrzehnte als Quasistandard in Sachen AI etablieren konnte. Noch heute findet ein Gutteil der Arbeit in Sachen künstlicher Intelligenz unter Verwendung von Lisp statt.
AI-Forschung ist allerdings nur eine Seite der Medaille. Lisp kommt nämlich nicht nur in diesem Sektor oder bei Viaweb zum Einsatz – es gibt Hunderte oder sogar Tausende von Firmen, die ihre Softwareentwicklung in dieser Programmiersprache bestreiten.

Wieso Lisp?

Wer Qt verwendet, tut dies normalerweise aufgrund der hohen Portabilität der Ergebnisse. Die Verwendung von Visual Basic geht – so zumindest die weit verbreitete Meinung – mit schnellen ersten Resultaten einher. C# gilt als die „Profisprache“ des .NET Frameworks. Die Befragung eines Lisp-Entwicklers liefert meist kein derart eindeutiges Ergebnis. Langjährige „Lisp-Hacker“ reagieren auf die Frage oft beleidigt: Wer nach dem Sinn von Lisp fragt, ist für die Verwendung der Programmiersprache unwürdig.
Der Name Lisp steht für List Processing. Peter Seibel bezeichnet die Sprache in seinem frei verfügbaren Lisp-Buch „Practical Common Lisp“ als „programmable programming language“. Das bedeutet, dass der Lisp-Programmierer die Sprache während der Erstellung seiner Applikation um neue Features ergänzen kann – dazu sind keine Compilererweiterungen oder neue Präprozessoren notwendig. Das liegt unter anderem daran, dass Lisp den Programmcode selbst als Datenstruktur betrachtet. Dadurch ist es möglich, selbsterstellenden Code zusammenzustellen: So genannte Makros generieren den auszuführenden Code on the fly.
Lisp-Programme bestehen aus Horden von Klammern. Deren für Quereinsteiger lästige Verwaltung wird heutzutage bereitwillig von den diversen IDEs erledigt. Wenn Sie sich ernsthaft mit der Sprache befassen wollen, so sollten Sie sich über kurz oder lang einen „intelligenten Editor“ Ihrer Wahl besorgen. Dabei ist Emacs übrigens nicht die erste Wahl: Aufgrund seiner enormen Komplexität erschwert seine Verwendung den Einstieg in die Welt von Lisp unnötig.

Geschichte von Lisp

Im Jahre 1960 veröffentlichte John McCarthy ein Whitepaper. In diesem beschrieb er den aus damaliger Sicht revolutionären Zustand, dass man eine Turing-komplette Sprache aus vergleichsweise wenigen und primitiven Operatoren zusammenstellen kann.
Der Programmierer Steve Russell las das Dokument und stellte fest, dass sich ein wichtiger Teil der Sprache auf der damals weit verbreiteten IBM 704 „direkt“ implementieren ließ. Bis heute finden sich Hinweise auf diese Architektur: Die Listenzugriffbefehle car und cdr wurden 1:1 aus der Assemblersyntax übernommen.
Aufgrund der enormen Hardwareanforderungen ließ sich Lisp auf den damaligen Computersystemen nicht ohne Weiteres implementieren. Dadurch entstand eine Gruppe von Firmen, die auf die Ausführung von Lisp spezialisierte Systeme anboten: Die als Lisp Machine bezeichneten Anlagen werteten ihren jeweiligen Dialekt der Sprache direkt als „Assemblercode“ auf Hardwareebene aus. Das Ende dieser (vergleichsweise inflexiblen) Computer leitete Moore’s Gesetz ein. Die meisten Anbieter sind mittlerweile bankrott, Symbolics lebt als Verkäufer einer Mathematiksoftware weiter.
Aufgrund der Vielzahl von Anbietern entstand eine unüberschaubare Anzahl von Dialekten. Diese wurden im Jahre 1984 in einem als „common Lisp“ bezeichneten Standard zusammengefasst, der bis heute Gültigkeit besitzt. Die „Kommerzialisierung“ von Lisp hatte den Nebeneffekt, dass die in den Anfangszeiten der Computertechnik bestehende „Hacker Culture“ am MIT ausgerottet wurde. Anfangs wollten die dort versammelten Hacker ihre LISP Machine gemeinsam vermarkten. Leider scheiterte dieses Vorhaben an Differenzen über die Art der Projektfinanzierung: Das Endresultat davon war, dass zwei konkurrierende Unternehmen namens Lisp Machines und Symbolics entstanden.
Der Gründer von Symbolics warb sodann die Mehrheit der Programmierer der ersten Generation ab – der darauffolgende Kampf zwischen Lisp Machines und Symbolics ist eine faszinierende Novelle. Eine witzige Randnotiz dieser Ereignisse ist, dass Richard Stalman durch das – aus seiner Sicht – „asoziale Verhalten“ von Symbolics zum Gründen des GNU-Projekts animiert wurde. Er verbrachte gut zwei Jahre seines Lebens mit dem Nachprogrammieren der Neuerungen des Unternehmens, die so auch für die von seinem Freund Greenblatt gegründete (und unterlegene) Firma zur Verfügung gestellt wurden.
Lisp Machines Inc meldete im Jahre 1979 Bankrott an und wurde danach vom kanadischen Investor Guy Montpetit wiederbelebt. Leider war dieser in einen Regierungsskandal involviert, der zur Beschlagnahmung aller Assets seiner Firmen führte. Das Endergebnis: Auch das reüssierende Unternehmen Lisp Machines ging den Weg alles Irdischen.
Konzepte wie die weit verbreitete GPL-Lizenz gehen auf die damaligen Vorfälle zurück: Selbst wenn Sie also Lisp nie direkt verwenden sollten, profitieren Sie als Informatiker indirekt trotzdem von den Leistungen der damaligen Community.

Erste Schritte in die Welt von Lisp

Nach diesen einführenden Kommentaren ist es an der Zeit, ein erstes kleines Progrämmchen zu generieren. Der unter Unix arbeitende Autor vollzieht die Installation der Arbeitsumgebung durch das Eingeben des Befehls sudo apt-get install cLisp. Nach dem Download des rund 2,5 MB großen Pakets steht der Interpreter bereit – die Eingabe von cLisp öffnet den in Abbildung 1 gezeigten interaktiven Interpreter.

Abb. 1: Der „Kronleuchter“ zeigt an, dass wir es hier mit einer Version von GNU Lisp zu tun haben

Unter Windows arbeitende Entwickler können auf eine Vielzahl von kommerziellen Lisp-Implementierungen zurückgreifen. Für erste Schritte ist die Windows-Version von GNU Lisp völlig ausreichend, die hier zum Herunterladen bereitsteht. Normalerweise erfolgt die Lisp-Programmierung „interaktiv“ an der Kommandozeile der Runtime. Als erstes „kleines Beispiel“ wollen wir eine einfache Addition realisieren. Diese erfolgt durch das Eingeben des Strings (+ 2 2). Wenn Sie diesen durch Betätigen von Enter quittieren, so liefert der Interpreter als Antwort „4“ zurück. Dieses per se nicht sonderlich beeindruckende Verhalten erlaubt uns erste Blicke in die Vorgehensweise des Lisp-Interpreters. Er ist vergleichsweise primitiv, da er eigentlich nur aus einer rekursiven Schleife besteht.
Solange Input zur Verfügung steht, liest der Interpreter den ersten Ausdruck ein. Dieser wird danach evaluiert, sein Ergebnis wird wieder in den Input zurückgeschrieben. Daraus folgt, dass Lisp nach Prefix-Notation arbeitet. Ein gutes Beispiel dafür ist das Lösen einer komplexen numerischen Berechnung (5 * cos(23) * (4 + 6)). Die korrekte Eingabenotation wäre (* 5 (cos 23) (+ 4 6)), wobei der Compiler die Aufrufe in „applicative order“ abarbeitet. Das bedeutet, dass die „Parameter“ evaluiert werden, bevor die letzte Funktion zum Aufruf kommt. Wichtig ist, dass Lisp auch die in den meisten anderen Programmiersprachen als Operatoren realisierten grundlegenden mathematischen Arbeiten über Funktionen darstellt.
Konditionale Operatoren
Jedes Lisp-Element ist als Präfixoperation angeschrieben. Die soeben erwähnte Methode der „applicative order“ wird in dem Moment zum Problem, in dem wir es mit einem konditionalen Operator zu tun bekommen. Als Beispiel dafür wollen wir die Codezeilen in Listing 1 analysieren, die hier mitsamt den Antworten der Runtime angegeben werden.

(if (= 3 3) (print "yes") (print "no"))

"yes" 

"yes"

(if (= 3 4) (print "yes") (print "no"))

"no" 

"no"

Die if-Funktion erwartet drei Parameter. Der erste ist die auszuführende Bedingung. So diese wahr ist, wird im nächsten Schritt der zweite Parameter ausgeführt. Trifft die Bedingung nicht zu, kommt stattdessen der Dritte zum Einsatz.
Normalerweise müsste die Abarbeitung des if-Befehls zur Ausgabe von yes und no führen. Das ist hier allerdings nicht der Fall, da if einer von rund zwei Dutzend „Sonderoperatoren“ ist. Diese haben die Eigenschaft, nicht alle an sie übergebenen Parameter sofort zu evaluieren und sind somit zum Realisieren von konditionalen Programmen geeignet.
Das Verhalten des =-Operators ist interessant. Er liefert im Wahrfall T, im Falschfall NIL zurück. Bei Zweitem handelt es sich um eine spezielle Konstante, die unter anderem auch zum Erstellen einer neuen, leeren Liste dient (Listing 2).

Break 4 [17]> (= 3 3)

T

Break 4 [17]> (= 3 4)

NIL

Aufmacherbild: Woman talking on the cell phone von Shutterstock / Urheberrecht: KPG Payless2

[ header = Seite 2: Funktionen und Listen ]

Funktionen und Listen

Wie erwähnt setzt sich der Name Lisp aus List und Processing zusammen. Das liegt daran, dass Listen einer der wenigen elementaren Datentypen der Sprache sind: Sowohl auf Seiten des weiter unten besprochenen Compilers als auch beim Erstellen des Codes erstellen sie regelmäßig Listen.
Das Eingeben des folgenden Befehls erstellt eine neue Liste, die parametriert ist. Das bedeutet, dass die einzelnen Listenelemente über einen „Namen“ ansprechbar sind:

Break 5 [18]> (list :name "R40" :familie "torpedo")

 (:NAME "R40" :FAMILIE "torpedo")

Wie schon beim Berechnen von mathematischen Operationen beginnt der Lisp-Parser auch hier mit der Analyse des Eingangsstroms. Wenn nur mehr ein „Fixdatenelement“ übrig ist, wandert dieses in die Konsole – in unserem Fall wäre das die fertige Liste. Leider ist das manuelle Erstellen derartiger Listen vergleichsweise ärgerlich. Es wäre weitaus angenehmer, wenn wir diese Aufgabe an eine darauf spezialisierte Funktion auslagern könnten. Das ist die Aufgabe des defun-Schlüsselworts, dessen Verwendung in unserem Fall so aussieht:

(defun make-entry (name familie)

(list :name name :familie familie))

MAKE-ENTRY

Wir verwenden hier die „Basisversion“ einer Funktion, deren Parameteranzahl fix vorgegeben ist. Bei komplexeren Programmen ist es oft wünschenswert, stattdessen auf Funktionen mit Default-Werten zurückzugreifen – deren Besprechung würde jedoch den Rahmen dieses Artikels sprengen. Die Abarbeitung einer Funktion erfolgt nach dem von normalen „Termen“ bekannten Schema. Das bedeutet, dass der Interpreter den übergebenen Code so lange abarbeitet, bis nur mehr ein Term übrigbleibt – dieser wird sodann retourniert.
Beim Eingeben dieses Befehls müssen Sie sich übrigens keine Sorgen um die Reihenfolge machen. Der Lisp-Interpreter reagiert auf das Drücken von Return nur dann mit der Ausführung des angegebenen Codes, wenn dieser eine balancierte Anzahl von öffnenden und schließenden Klammern aufweist (Abb. 2).

Abb. 2: Das Drücken von Return führt nicht zur Programmausführung, da eine ) fehlt

Neue Funktion benutzen

Nach dem Abarbeiten des defun-Statements haben wir eine neue Funktion angelegt. Diese sitzt im Speicher des Interpreters, bis dieser neu gestartet wird. Aus der Logik folgt, dass sich die Methode zur Eintragserstellung wie gewohnt ansprechen lässt:

(make-entry "bisnovat" "rakete")

(:NAME "bisnovat" :FAMILIE "rakete")

Leider sind wir damit noch nicht am Ziel. Die von make-entry generierte Liste verpufft nämlich in der Konsole. Ihre Weiterverwendung setzt das Anlegen einer globalen Variable voraus. Dazu ist der folgende Code notwendig:

(defvar *mylist* nil)

*MYLIST*

Der Opcode defvar – die Abkürzung steht für DEFine VARiable – hat die Aufgabe, neue Variablen zu erstellen. Wir nennen unseren globalen Speicher *mylist* und versorgen ihn mit einem leeren Initialwert.
Es gilt unter Lisp-Programmierern als guter Ton, die Namen von globalen Variablen mit Sternchen zu umgeben. Das ist allerdings keine Verpflichtung, die meisten Runtimes akzeptieren die Deklaration auch mit einem „normalen“ Namen.
Lisp bietet eine Vielzahl von Funktionen an, die die Verwaltung der in einer Liste befindlichen Daten erleichtern. Zum Hinzufügen neuer Tupel kommt normalerweise push zum Einsatz – das sieht in der Praxis so aus:

(push (make-entry "Eintrag" "Familie") *mylist*)

((:NAME "Eintrag" :FAMILIE "Familie"))

Push verlangt zwei Parameter. Der erste enthält die in die Liste zu schreibenden Daten, der zweite die die Liste enthaltende Variable. Damit stellt sich eigentlich nur mehr die Frage, wie wir auf die in der Datenbank befindlichen Informationen zugreifen können. Die „schnellste“ Form der Ausgabe ist der gesamte Variableninhalt, der durch das Eingeben des Variablennamens in die Konsole geschrieben wird:

*mylist*

((:NAME "Eintrag" :FAMILIE "Familie"))

Alternativ können Sie die Liste auch mit einem Iterator durchlaufen und die einzelnen Einträge mittels Print oder Format in die Konsole ausgeben. Die dazu notwendigen Befehle würden den Rahmen dieses Artikels sprengen, weshalb wir uns hier mit der „einfachen Variante“ zufriedengeben wollen.

[ header = Seite 3: Was passiert im Hintergrund? ]

Was passiert im Hintergrund?

Lisp-Gegner entschlüsseln den Namen der Programmiersprache normalerweise als „Lots of Irritating Superfluous Parentheses“. Das liegt daran, dass Code auf den ersten Blick wie eine wirre Ansammlung von Klammern aussieht, denn die Deklarationen der „Listen“ erfolgen durch die Klammern. Dieser Aufbau sorgt für die enorme Flexibilität der Sprache. Normalerweise wird eine Programmiersprache von einem Compiler ausgeführt, der für den Entwickler mehr oder weniger wie eine Blackbox aussieht. Lisp wird etwas anders abgearbeitet. Im ersten Schritt steht ein Prozess, der die eingegebenen Texte in als S-Expressions bezeichnete Lisp-Objekte umwandelt. Dabei handelt es sich um eine Abart des Syntaxbaums, der in anderen Programmiersprachen vom Compiler vor dem User versteckt wird.
Bei geschickter Verwendung von Makros erlaubt Lisp das Realisieren ganzer Domain-spezifischer Programmiersprachen (DSLs). Wir wollen uns an dieser Stelle mit weniger komplizierten Befehlen befassen und ein vergleichsweise primitives Konstrukt realisieren – einen when-Befehl:

(defmacro suswhen (condition &rest body)
  '(if ,condition (progn ,@body)))

Die Verwendung der backquote und des Kommas weisen die Engine dazu an, Teile des an sie übergebenen Strings nicht zu verarbeiten. Nach dem Eingeben des hier gezeigten Befehls steht Ihnen der when-Befehl wie jede beliebige andere Funktion zur Verfügung. Das Übergeben einer Bedingung und eines auszuführenden Codeteils führt zum erwarteten Resultat.
Die Ausführung eines in Lisp gehaltenen Programms erfolgt in zwei Stufen. Im ersten Schritt analysiert der Interpreter die Makros in einem als „Macro Expansion Time“ bezeichneten Betriebsmodus. Dabei geht es nur darum, die Makros in Lisp-Code zu expandieren. Dieser wird erst im nächsten, als „Runtime“ bezeichneten Schritt abgearbeitet.
Das Verwenden von Makros ist einer der wichtigsten Teilaspekte von Lisp. Die meisten Interpreter werden mit einer gigantischen Auswahl von Makros ausgeliefert – die hier erstellte suswhen-Order ist normalerweise schon als when implementiert.
Wirklich sinnvoll ist die Verwendung eines Makros immer dann, wenn sich dadurch eine große Menge von weitgehend gleichartigem Code eliminieren lässt. Weitere Informationen dazu finden Sie im Makro-Kapitel von Peter Seibel’s lesenswertem und im nächsten Absatz angeführten zweiten Lisp-Buch.

Lisp, kompiliert

Das schrittweise Eingeben von Code mag für erste Experimente sinnvoll sein. Zum Erstellen größerer Programme ist es nicht wirklich hilfreich – für „gut befundene“ Funktionen, Makros und Konstrukte sollten von der Runtime automatisch importiert werden.
Als ersten Schritt zur Steigerung der Effizienz wollen wir das Laden von Lisp-Dateien betrachten. Dabei versorgen Sie die Runtime mit einer Datei, die eine Gruppe von Eingaben enthält. Diese werden vom Compiler dann wie „echte Benutzereingaben“ abgearbeitet. Enthält Ihre Datei beispielsweise eine Gruppe von defun-Befehlen, so würden die dadurch erstellten Funktionen danach zum Aufruf bereitstehen: (load „beispiel.Lisp“). Bei sehr großen Programmen ist die für die Kompilation der Eingaben notwendige Zeit lästig. Dieses Problem lässt sich durch das Verwenden des compile-file-Befehls umgehen, dessen Syntax so aussieht: (compile-file „beispiel.Lisp“). Nach der erfolgreichen Abarbeitung des Kommandos finden Sie auf Ihrer Workstation eine zusätzliche Datei mit der Endung .fasl. Diese lässt sich danach durch den load-Befehl nach folgendem Schema in den Speicher holen:

(load "testing")
; Fast loading ./testing.fasl

Die Anzeige von Fast Loading weist Sie darauf hin, dass der normalerweise notwendige Kompilationsschritt bei diesem spezifischen Aufruf ersatzlos entfällt.

Lisp lernen

Wenn Sie sich nach den hier gezeigten ersten Schritten weiter mit Lisp befassen möchten, steht Ihnen eine Vielzahl von Ressourcen zur Verfügung. Als Standard gilt „Common Lisp the Language“ – dieses vergleichsweise umfangreiche englische Buch steht hier im Volltext bereit. Peter Seibels bei Apress erschienenes Buch „Practical Common Lisp“ ist vom Aufbau her für Quereinsteiger zugänglicher. Es steht hier zum kostenlosen Download bereit.
Die Websuche nach „Lisp Tutorial“ oder „Lisp Course“ bringt diverse Papers und Webseiten von amerikanischen Universitäten ans Tageslicht. Die dort angebotenen Informationen reichen vom Umfang her zwar nicht an die oben genannten Bücher heran, führen Anfänger aber trotzdem an der einen oder anderen Falle vorbei.
Zu guter Letzt gibt es viele lebendige Lisp-Foren. Aufgrund der vergleichsweise geringen Anzahl von Lisp-Programmierern ist die Mehrheit der Nutzer über „Neukunden“ erfreut – auf Stack Overflow finden Sie bei kleineren Problemen ebenfalls Rat und Hilfe.

Scheme? Scheme!

Wer sich mit Lisp beschäftigt, trifft über kurz oder lang auch auf Scheme. Dabei handelt es sich um einen eng mit der Muttersprache verwandten Dialekt, der sich aber in Sachen Designparadigmata unterscheidet. Während Common Lisp versuchte, im Sprachstandard so viel wie möglich abzudecken, folgt Scheme eher dem Ansatz des GNU-Projekts. Das Ziel der Entwickler war das Anbieten eines kompakten Sprachkerns, der sich schnell und unbürokratisch um weitere Funktionen erweitern ließ. Aufgrund der starken akademischen Wurzeln der Entwickler (und des vergleichsweise geringen Umfangs) erfreute sich Scheme schon bald großer Popularität. Das im englischsprachigen Raum weit verbreitete Systemtechniklehrbuch „Structure and Interpretation of Computer Programs“ nutzt Scheme als Lehrsprache, was zu einer konstanten Verbreitung des Dialekts führt.

Fazit

Es gibt – mit Ausnahme von Ruby – kaum eine weit verbreitete Programmiersprache, die auch nur periphere Ähnlichkeiten mit Lisp aufweist. Da das menschliche Gehirn bei der Beschäftigung mit anderen Themenfeldern „windfall profits“ (zu Deutsch Marktlagengewinne) generiert, lohnt sich ein Blick auf die Sprache schon allein aus diesem Grund. Die durch Lisp gesammelten Eindrücke und Erfahrungen wirken sich nämlich auch an „anderer Stelle“ aus.
Aufgrund der geringen Anzahl von guten Lisp-Programmierern könnte es sich auch aus karrieretechnischer Sicht auszahlen, ein wenig Zeit mit dem Listenverarbeiter zu verbringen. Wer sich heute mit in die Jahre gekommenen Sprachen wie COBOL und Co. gut auskennt, verdient mitunter hohe dreistellige Stundenhonorare: Dem Autor dieser Zeilen ist eine COBOL-Programmiererin bekannt, die mit der Wartung eines antiken Bankensystems ein hohes sechsstelliges Einkommen erwirtschaftet.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -