Logo von Developer

Suche
preisvergleich_weiss

Recherche in 2.354.050 Produkten

Golo Roden 110

Lisp - der Geheimtipp unter den Programmiersprachen

Lisp - Der Geheimtipp unter den Programmiersprachen

Der US-amerikanische Entwickler Eric S. Raymond hat einmal gesagt, Lisp sei selbst dann eine lernenswerte Sprache, wenn man sie niemals verwenden werde. Der Erkenntnisgewinn führe dazu, ein allgemein besserer Entwickler zu werden.

Die Softwareentwicklung des vergangenen Vierteljahrhunderts wurde stark von den Programmiersprachen der C-Familie geprägt. Seit dem Erscheinen von C und C++ in den Jahren 1972 und 1985 haben zahlreiche andere Sprachen das Licht der Welt erblickt, die allesamt von ihnen inspiriert sind. Dazu zählen auch Java und C#, die für zahlreiche Entwickler zu den Alltagssprachen schlechthin geworden sind.

Auf den ersten Blick könnte man meinen, dass nahezu alle Sprachen, die seither erschienen sind, mit C und C++ verwandt sind, abgesehen von den unterschiedlichen Dialekten von BASIC, die ebenfalls eine gewisse Reichweite erlangt haben. Der Eindruck trügt allerdings, denn tatsächlich gibt es einen wahren Fundus – nicht nur an anderen Sprachen, sondern vor allem an gänzlich anderen Programmiersprachenkonzepten.

Dazu zählen unter anderem diverse funktionale Sprachen, beispielsweise Haskell und F#. Auch das inzwischen weit verbreitete JavaScript sieht nur äußerlich wie eine Sprache der C-Familie aus, tatsächlich ist es viel stärker von anderen Sprachen beeinflusst. Eine davon ist die 1958 erschienene Sprache Lisp. Der Name ist ein Akronym, das für List Processing steht. Demnach handelt sich bei Lisp offensichtlich um eine Spezialsprache zur Verarbeitung von Listen. Das ist einerseits richtig, verkennt andererseits aber die wahren Qualitäten der Sprache: Das Besondere an Lisp ist nicht, dass es gut mit Listen umgehen kann, sondern die sich daraus ergebenden Konsequenzen.

Neun Gründe für Lisp

Paul Graham, Entwickler und Gründer des Risikokapitalgebers Y Combinator, hat zu ebendiesem Thema einen Blogeintrag mit dem Namen "What Made Lisp Different" verfasst, in dem er neun Ideen beschreibt, die Lisp auszeichnen. Einige davon haben im Lauf der Zeit ihren Weg in die gängigen Mainstream-Sprachen gefunden, andere sind nach wie vor exklusiv Lisp vorbehalten.

Als erste Idee führt Paul Graham an, dass Lisp als erste Programmiersprache eine echte if-Anweisung kannte. John McCarthy, der Erfinder von Lisp, berichtet in "LISP prehistory – Summer 1956 through Summer 1958", dass weder das damals verfügbare Fortran noch die darauf aufbauende Fortran List Processing Language (FLPL) die Möglichkeiten besaßen, Bedingungen als Ausdruck zu formulieren:

"While expressions could be handled easily in FLPL [...], it had neither conditional expressions nor recursion, [...]"

Neue Artikelreihe

Es muss nicht immer Java oder C sein: In dieser neuen Reihe stellen unsere Autoren ihre liebsten Programmiersprachen abseits des Mainstreams vor.

Tatsächlich glich die in Fortran enthaltene if-Anweisung einem bedingten Sprung, wie er heute noch in Assembler verwendet wird. McCarthy berichtet weiter, dass für Fortran deshalb rasch die XIF-Funktion entwickelt wurde, die einen von zwei übergebenen Ausdrücken als Rückgabewert zurücklieferte. Da sie jedoch als normale Fortran-Funktion implementiert war, wurden stets beide Ausdrücke ausgewertet:

"The function shortened many programs and made them easier to understand, but it had to be used sparingly, because all three arguments had to be evaluated before XIF was entered, since XIF was called as an ordinary FORTRAN function though written in machine language."

Das änderte sich 1958 mit dem Erscheinen von Lisp, das im heutigen Sinne "richtige" Bedingungen enthielt, die nur einen der beiden übergebenen Ausdrücke auswerten. Das Verhalten ist bis heute geblieben und in jeder modernen Programmiersprache üblich.

Funktionen als Datentypen erster Klasse

Die zweite Idee ist, Funktionen als Datentypen erster Klasse anzusehen, ebenso wie Zeichenketten oder Zahlen. Das bewirkt, dass sich Funktionen wie Daten verarbeiten lassen, da sie als Parameter an andere Funktionen übergeben oder als Rückgabewert von diesen zurückgegeben werden können. Dieser Ansatz bildet beispielsweise die Grundlage für den von Google verwendeten Algorithmus MapReduce, der das effiziente Verarbeiten großer Datenmengen ermöglicht.

Für Entwickler ist dabei ersichtlich, wie die Daten tatsächlich verarbeitet werden. Ist beispielsweise eine Liste von Zahlen zu quadrieren, gibt man lediglich die Funktion an, die die eigentliche Berechnung durchführt. Wie die Liste tatsächlich durchlaufen wird, entscheidet die map-Funktion, mit der sich der Vorgang entsprechend der zur Verfügung stehenden Hardware oder auch im Hinblick auf andere Faktoren optimieren lässt.

Daher wäre es zum Beispiel möglich, die map-Funktion zu parallelisieren und dadurch zu beschleunigen, ohne dass der Entwickler das wissen oder beachten müsste. Genau das beschreibt auch Entwickler Joel Spolsky in seinem Blogeintrag "Can Your Programming Language Do This?", wenn er schreibt:

"And now you understand something I wrote a while ago where I complained about CS students who are never taught anything but Java: Without understanding functional programming, you can't invent MapReduce, the algorithm that makes Google so massively scalable."

Er zieht daraus den Schluss, dass die funktionale Programmierung hilft, das Abstraktionsvermögen einer Sprache zu verbessern, und dass sich Code kompakter sowie besser wiederverwend- und skalierbar schreiben lässt, wenn Funktionen als Bürger erster Klasse angesehen werden:

"Ok. I hope you're convinced, by now, that programming languages with first-class functions let you find more opportunities for abstraction, which means your code is smaller, tighter, more reusable, and more scalable. Lots of Google applications use MapReduce and they all benefit whenever someone optimizes it or fixes bugs."

Rekursion

Die dritte Idee, die in Lisp erstmals umgesetzt wurde, ist die der Rekursion. Das wirkt auf den ersten Blick seltsam, da das Konzept so naheliegend zu sein scheint. Allerdings unterstützt selbst heute nicht jede Sprache das Konzept im vollen Umfang. JavaScript kannte vor ECMAScript 2015 beispielsweise keine Endrekursion, andere Sprachen wie F# und OCaml können zwar mit Rekursion umgehen, man muss sie jedoch explizit anfordern. In F# beispielsweise dient dazu das Schlüsselwort rec: Fehlt es bei der Definition einer Funktion, ist es für sie nicht möglich, sich selbst aufzurufen.

Der Beitrag "Why are functions in Ocaml/F# not recursive by default?" auf Stack Overflow nennt als primären Grund dafür, dass es dadurch möglich ist, Funktionen neu zu definieren, ohne die Zugriffsmöglichkeit auf die ursprüngliche Funktion zu verlieren:

"Functions are not recursive by default in the French CAML family of languages (including OCaml). This choice makes it easy to supercede function (and variable) definitions using let in those languages because you can refer to the previous definition inside the body of a new definition. F# inherited this syntax from OCaml."

Da sich jedes rekursive Problem auch iterativ lösen lässt, schränkt die fehlende Verfügbarkeit des Konzepts Entwickler nur bedingt ein. Es gibt aber zahlreiche Aufgaben, die sich rekursiv einfacher lösen lassen. Man denke dabei an Probleme wie die Berechnung der Fibonacci-Zahlen oder der Fakultät.

Dynamische Typisierung und Co.

Die vierte Idee, die in Lisp erstmals implementiert wurde, ist die eines dynamischen Typsystems. Generell lassen sich Typsysteme auf vielerlei Art einteilen: statisch, dynamisch, streng, schwach, explizit, implizit, nominal, strukturell, und so weiter.

Die erste höhere Programmiersprache, Fortran, basierte auf einem statischen Typsystem. Von dort vererbte sich dessen Verwendung über Algol nach C, was schließlich viele der heute verbreiteten Sprachen wie C++, Java und C# beeinflusste. Das Verwenden eines statischen Typsystems stellt sicher, dass ein Typ bereits zur Übersetzungszeit bekannt ist. Das kann auf zwei Weisen erfolgen: Entweder müssen Entwickler die verwendeten Typen explizit benennen, oder der Compiler ermittelt sie eigenständig. Im ersten Fall spricht man von einem expliziten, im zweiten von einem impliziten Typsystem.

Verhältnismäßig unbekannt sind die nominale und die strukturelle Typisierung, die sich allerdings leicht abgrenzen lassen. Eine Sprache mit nominaler Typisierung unterscheidet Typen anhand ihrer Namen, eine mit struktureller Typisierung hingegen anhand ihrer Struktur: Sind zwei Typen gleich aufgebaut, gelten sie als kompatibel. Ein aktuelles Beispiel für eine Sprache mit struktureller Typisierung ist TypeScript.

In Lisp hingegen ließ sich ein Datentyp erstmals erst zur Laufzeit festlegen, um beispielsweise den Rückgabetyp einer Funktion an die jeweiligen Anforderungen anzupassen. Lisp agiert dabei jedoch weitaus strenger als zum Beispiel JavaScript, was die Kompatibilität von Typen angeht. Möglich wurde das durch den Trick, den Typ eines Ausdrucks nicht an der Variablen festzumachen, sondern am Wert: Dadurch können sich die Typen von Variablen ändern, da sie lediglich als Verweis auf einen konkreten, typisierten Wert dienen.

Garbage Collection

Die fünfte Idee, die erstmals in Lisp umgesetzt wurde, ist die einer automatischen Speicherverwaltung. Im Hinblick auf die Probleme, die unter anderem in C und C++ durch manuell verwalteten Speicher bestehen, verwundert es, dass Lisp bereits 1958 über eine Garbage Collection verfügte. Die Idee, Entwickler von der mühsamen und fehleranfälligen Verwaltung des Speichers zu entbinden, haben gängige Sprachen vor allem durch den Einsatz von verwalteten Laufzeitumgebungen wie Java oder .NET wiederbelebt.

Die bis hierhin behandelten ersten fünf Ideen stellen heute keine Spezialitäten von Lisp dar. Obwohl nicht jede moderne Sprache alle Ansätze in exakt der Form umsetzt wie Lisp, können sie doch als im Mainstream angekommen gelten. Anders verhält es sich mit den übrigen Ideen, die inzwischen zwar zumindest teilweise auch von der ein oder anderen Programmiersprache aufgegriffen werden, die aber noch nicht wirklich Eingang in den Alltag der meisten Entwickler gefunden haben.

Ausdrücke ersetzen Anweisungen

Die erste dieser Ideen und somit die sechste in der Zählung ist, komplett auf Anweisungen zu verzichten und Programme aus Ausdrücken aufzubauen. Das wirkt auf den ersten Blick nicht sonderlich schwerwiegend, ermöglicht jedoch eine viel einfachere Parallelisierung von Code.

Beim direkten Vergleich fällt auf, dass Ausdrücke stets den gleichen Wert repräsentieren, unabhängig davon, wie oft sie verwendet werden. So ergibt der Ausdruck 2 + 3 stets den Wert 5. Auch die Reihenfolge der Evaluation der Ausdrücke ist unerheblich, sofern sie nicht voneinander abhängen. Das Ergebnis des arithmetischen Ausdrucks

(2 + 3) * (5 + 7)

ergibt stets 60, unabhängig davon, welche Summe zuerst berechnet wird. Die Berechnung der Summen könnte sogar parallel erfolgen, da das jeweilige Einzelergebnis unabhängig vom anderen ist. Code, der ausschließlich aus Ausdrücken besteht, lässt sich daher vom Compiler und der Laufzeitumgebung ausgezeichnet parallelisieren, ohne dass Entwickler hierfür Maßnahmen ergreifen müssen. Anweisungen hingegen verändern den Zustand der Anwendung bei jedem einzelnen Aufruf erneut.

Zwei Anweisungen, die beispielsweise einen Wert auf den Bildschirm ausgeben, scheinen zwar ebenfalls unabhängig voneinander zu sein, tatsächlich bauen sie allerdings aufeinander auf. Die Ausgabe der Anwendung hängt von der strikt sequenziellen Ausführung der Anweisungen ab. Code, der aus Anweisungen besteht, lässt sich daher nicht ohne explizite Hinweise durch den Entwickler parallelisieren.

Das Konzept, auf Anweisungen zu verzichten und sie durch Ausdrücke zu ersetzen, fördert auch den Einsatz der funktionalen Programmierung: Da eine Funktion nur noch aus einem Baum von Ausdrücken besteht, entspricht sie eher einem zu evaluierenden Wert als einer Liste von Anweisungen, die das Programm prozedural nacheinander verarbeitet.

Symbole

Die siebte Idee, die ihren Weg inzwischen beispielsweise nach JavaScript gefunden hat, ist das Konzept von Symbolen. Auf den ersten Blick scheinen Symbole das Gleiche zu sein wie Strings, da beide als Zeichenketten dargestellt werden.

Der Unterschied liegt darin, wie sie intern gespeichert und verglichen werden: bei Strings kommen ihre Werte, bei Symbolen ihre Referenzen zum Einsatz. Das bedeutet, dass die gleiche Zeichenkette durchaus zweifach im Speicher vorhanden sein kann, das gleiche Symbol hingegen nur ein einziges Mal. Im Gegensatz zu Strings sind Symbole deshalb eindeutig, weshalb sie sich auch leicht mittels Referenzvergleichs prüfen lassen.

Wirklich interessant werden Symbole erst, wenn sich beliebiger Code mit ihrer Hilfe darstellen lässt. In dem Moment, in dem Code als Menge von Symbolen aufgefasst wird, lässt sich Code als Daten schreiben: Code und Daten nutzen dann die gleichen elementaren Bausteine, die ihre Grundlage sind.

Code und Daten als AST

Genau das ist die achte Idee: Lisp behandelt Code und Daten identisch, die Unterscheidung zwischen beiden ist rein willkürlich. Beide Konstrukte verwenden die gleiche Syntax, was dazu führt, dass Lisp sowohl Code als auch Daten intern letztlich als abstrakten Syntaxbaum (AST) darstellt. Für Code handhabt das jede Sprache ohnehin so. Neu ist aber die Idee, das Vorgehen auf Daten auszuweiten.

Denkt man den Ansatz zu Ende, gibt es keinen Unterschied zwischen beidem mehr, wie es etwa auch bei XML der Fall ist. Ob der Codeschnipsel

<add>
<number>2</number>
<number>3</number>
</add>

einen Funktionsaufruf für eine dazu passende Laufzeitumgebung darstellt oder ob es sich lediglich um eine baumartige Datenstruktur handelt, lässt sich ohne Kontext nicht entscheiden. Spannend sind aber die Möglichkeiten, die sich aus dem Gedanken ergeben: Wenn Code und Daten ein- und dasselbe sind, lässt sich Code wie Daten manipulieren. Programme, die Programme schreiben, sind in einer derartigen Sprache leicht zu implementieren – schließlich gilt es dazu lediglich, Datenstrukturen zu verändern.

Die Sprache ist immer verfügbar

Die neunte Idee treibt diesen Ansatz schließlich auf die Spitze, indem die ständige Verfügbarkeit der gesamten Sprache gegeben ist. Das bedeutet, dass Lisp nicht nur zur Kompilierzeit zur Verfügung steht, sondern beispielsweise auch zur Laufzeit oder zur Ladezeit. Dadurch lassen sich zudem die Übersetzung und die Ausführung an sich beeinflussen, sodass eine programmierbare Programmiersprache entsteht, die sich mit wenig Aufwand beispielsweise um eigene Sprachkonstrukte erweitern lässt. Wer das bereits in einer Sprache aus der C-Familie versucht hat, weiß, dass man davon dort noch nicht einmal ansatzweise träumen darf.

Überraschend mag wirken, dass diese neun Ideen nicht eine Entwicklung der jüngeren Zeit sind, sondern seit fast sechzig Jahren zur Verfügung stehen, wenn auch in einer verhältnismäßig selten anzutreffenden Programmiersprache. Das liegt letztlich daran, dass die Sprache stark am Lambda-Kalkül aus der theoretischen Informatik ausgerichtet ist und sich auf einige wenige Konstrukte konzentriert, statt zu versuchen, durch zahllose Schlüsselwörter zu glänzen. Gerade der Minimalismus von Lisp ermöglicht erst all diese Fähigkeiten.

Ungewohnt bei Lisp ist zu Beginn fast immer die Syntax, die stark von dem abweicht, was man von beispielsweise Java und C# gewohnt ist. Auf den ersten Blick fallen die vielen Klammern auf, die Listen eingrenzen. Das wirkt ungewohnt, sorgt aber für eine hohe Flexibilität, die das folgende Beispiel zeigt. Der Code definiert zunächst die factorial-Funktion und ruft sie anschließend auf:

(defun factorial (n)
(if (<= n 1)
1
(* n (factorial (1- n)))))

(factorial 5) ; => 120

Lässt man sich auf die Syntax ein, fällt der Einstieg allerdings vergleichsweise leicht. Als hervorragendes Buch für den Einstieg sei "Common Lisp: A Gentle Introduction to Symbolic Computation" von David S. Touretzky empfohlen, das im Jahr 1990 erschienen ist. Einen ersten Eindruck können außerdem die beiden Blogeinträge "Primzahlen berechnen" und "Quicksort implementieren" vermitteln.

Fazit

Alles in allem ist Lisp eine bemerkenswerte Programmiersprache, die trotz ihres hohen Alters viele Konzepte enthält, die selbst moderne Programmiersprachen nicht alle zu bieten haben. Insbesondere der Ansatz der Metaprogrammierung ist spannend und eröffnet nicht nur ganz neue Themenfelder, sondern auch interessante neue Denkansätze.

Auffällig ist, dass aktuelle Sprachen zunehmend mehr dieser Ideen übernehmen, letztlich werden sie damit Lisp aber immer ähnlicher. Wer tatsächlich den letzten Schritt gehen will und die identische Darstellung von Code und Daten fordert, schafft praktisch eine Sprache, die Lisp gleicht. So gesehen ist es schade, dass Lisp nur eine dermaßen geringe Verbreitung beschieden ist.

Trotzdem empfiehlt es sich, der eingangs angesprochenen Aussage von Eric S. Raymond zu folgen, und sich Lisp zumindest gelegentlich aus Neugier anzuschauen. Der Lerneffekt, auch für den Einsatz anderer Sprachen, ist aufgrund des starken konzeptionellen Charakters sehr hoch. (jul)

Golo Roden
ist Gründer, CTO und Geschäftsführer der the native web GmbH, eines auf native Webtechnologien spezialisierten Unternehmens. Für die Entwicklung moderner Webanwendungen bevorzugt er JavaScript und Node.js und hat mit "Node.js & Co." das erste deutschsprachige Buch zum Thema geschrieben.

110 Kommentare

Themen: