Lisp - der Geheimtipp unter den Programmiersprachen

Seite 2: Dynamische Typisierung und Co.

Inhaltsverzeichnis

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.

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.

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.

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.

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 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.