Unterschiedliche Konzepte sind nicht mit einer einzigen Programmiersprache zu realisieren, aber:

Flexibilität von Lisp ist nach wie vor unerreicht

06.02.1987

Die Entwickler von Expertensystemen und entsprechenden Software-Tools greifen zunehmend auf die Sprache "Prolog" zurück. Bislang stand das bereits 30 Jahre alte "Lisp" in der Gunst der Programmierschmieden ganz oben. Gerhard Barth vergleicht vier verschiedene Sprachkonzepte vor allem hinsichtlich ihrer Eignung für die Implementierung wissensbasierter Systeme (Teil 1).

Die Entwicklung der Sprache "C" ist eng an das Betriebssystem Unix gekoppelt, dessen Code zu 90 Prozent in "C" abgefaßt ist. Sie hat sich als eine vorzügliche Programmiersprache für die Systemimplementierung erwiesen. Damit ist bereits das derzeit wichtigste Anwendungsgebiet genannt. Jedoch werden zunehmend auch Probleme der alphanumerischen Datenverarbeitung in "C" codiert.

"C" folgt dem Paradigma der prozeduralen Programmierung und beruht auf dem traditionellen Von-Neumann-Modell. Der Programmentwickler hat dabei die Formulierung eines Prozesses im Sinn, dessen Einzelschritte aus Änderungen der Speicherbelegung bestehen.

Ausgehend von diesem Modell muß "C" Ausdrucksmittel für die Strukturierung von Daten ("Datentypen", für die Vereinbarung von Operationen ("Unterprogramme"), für die Steuerung von Operationen ("Kontrollstrukturen") und für die Zuleitung von Daten an Operationen ("Parameter") enthalten.

Als elementare Datentypen stellt "C" ganze Zahlen ("int", "short", "long"), rationale Zahlen ("float", "double") und Zeichen ("char") zur Verfügung. Die Bildung zusammengesetzter Datentypen kann mittels mehrdimensionaler Felder, das heißt Strukturen ("struct") oder Vereinigungen ("union"), vorgenommen werden. Darüber hinaus erlaubt die Verwendung von Zeigern ("Adressen") den Aufbau komplexer Datenstrukturen wie Listen, Bäume und Graphen.

Neben Anweisungen zur Auswertung von Ausdrücken ("expression statements") enthält "C" Ausdrucksmittel zur Gruppierung von Anweisungen ("compound statements"), zur Auswahl von Anweisungen ("switch", "if...", "if ..." "else ..."), zur Wiederholung von Anweisungen ("for", "while", "do ... while") und zur Umlenkung des Kontrollflusses ("goto ", "break", "continue", "return").

Die Ein- und Ausgaben von "C" beziehen sich üblicherweise auf die Standarddateien "stdin" und "stdout". Diese Dateien werden im allgemeinen mit den Funktionen "getchar", "putchar", "scanf" und "printf" bearbeitet. Die beiden erstgenannten dienen der Übertragung einzelner Zeichen, während mit den beiden anderen Funktionen die Ein- und Ausgabe von Zahlen, Zeichenketten und anderen Daten gesteuert werden kann. In die Durchführung von Ein/Ausgabe-Operationen können auch andere als die genannten Standarddateien einbezogen werden. Dabei läßt sich besonders in Kombination mit Unix die Übertragung von Daten zwischen verschiedenen Dateien bequem durchführen.

Wie in anderen prozeduralen Programmiersprachen werden auch in "C" Programme in kleinere Unterprogramme ("Funktionen") zerlegt. Neben einer Vielzahl vordefinierter Standardfunktionen, die in Bibliotheken gelagert werden, hat der Entwickler natürlich die Möglichkeit, problemspezifische Funktionen zu vereinbaren. Sie werden nach folgendem Muster aufgebaut:

- Resultatstyp Name (Parameter)

- Beschreibung der Parametertypen

- lokale Variablenvereinbarungen

- Anweisungen

Schachtelungen von Funktionen ineinander sind nicht erlaubt, wohl aber rekursive Funktionsaufrufe. Die Funktionen können auf gemeinsame externe Datenbereiche zurückgreifen. Ihre eigenen Daten werden zumeist automatisch beim Aufruf angelegt und beim Verlassen wieder gelöscht ("automatic"). Lokale Daten können aber auch als statisch vereinbart werden ("static"); sie bleiben dann über die Aktivierung einer Funktion hinaus erhalten. Argumente können bei Funktionsaufrufen entweder über ihren Wert ("call by value") oder über ihre Adresse ("call by reference") übergeben werden.

"C" hat sich bei der Erstellung komplexer Softwaresysteme, zum Beispiel Unix, als Implementierungssprache sehr gut bewährt. Insbesondere die Möglichkeiten zur bequemen Einbeziehung von Dateien, zum Aufbau und Zugriff auf Funktionsbibliotheken sowie zur getrennten Übersetzung von Funktionen spielen dabei eine positive Rolle. Die gute Portabilität und der bei den meisten Implementierungen effiziente Umgang mit Betriebsmitteln sind weitere Vorteile der Programmiersprache.

Die Sprache "Lisp" hingegen baut auf dem Muster der applikativen Programmierung auf: Der Entwickler ist in erster Linie mit der Anwendung ("Applikation") von Operatoren auf Eingabedaten "E" befaßt; dadurch entstehen Ausgabedaten "A".

Der Anwendung eines Operators entspricht die Auswertung eines Ausdrucks, wobei anstelle formaler Parameter die durch "E" gekennzeichneten Argumente verwandt werden. Im allgemeinen zerfallen Ausdrücke in Teilausdrücke, die dann durch Berechnungsschemata wie Komposition ("nacheinander auswerten"), Selektion ("wahlweise auswerten") oder Rekursion

("verzahnt auswerten") zusammengefügt werden.

Es ist üblich, Teilausdrücke mehrfach zu verwenden. Aus diesem Grund ist es sinnvoll, Ausdrücke mit Namen zu versehen, um sie bei Bedarf unter Verwendung ihres Namens ansprechen zu können. Diese Vorgehensweise führt zu den Begriffen "Funktionsvereinbarung" und "Funktionsanwendung", weshalb "Lisp" manchmal fälschlicherweise als funktionale Programmiersprache bezeichnet wird.

"Lisp" ist eine interaktive Programmiersprache: Jeder Anwender durchläuft im Dialog mit dem Laufzeitsystem ständig einen dreistufigen Arbeitszyklus (siehe Abbildung 1). Typischerweise werden dabei zunächst Funktionen vereinbart, um diese später in die Ausrechnung komplexer Ausdrücke einbeziehen zu können.

Der Name "Lisp" ist abgeleitet von List Processing. Damit wird angedeutet, daß Listen das wichtigste Hilfsmittel zur Strukturierung von "Lisp"-Programmen sind. Tatsächlich sind

Listen nichts anderes als eine spezielle Form von Ausdrücken, allerdings die mit Abstand häufigste Variante.

Listen sind nach dem Muster (a b c ...) aufgebaut, wobei als Listenelemente Ausdrücke verwandt werden. Das hat zur Folge, daß Listen in andere Listen geschachtelt werden können.

Da Listen auch Ausdrücke sind, müssen sie einer Auswertung unterworfen werden können - und zwar dadurch, daß das erste Element einer Liste als Funktion und die restlichen Elemente als Argumente dieser Funktion aufgefaßt werden. Das bedeutet beispielsweise, daß das Resultat der Liste (DREHE RECHTECK 90 RICHTUNG) durch die Anwendung einer zuvor vereinbarten Funktion DREHE auf die drei Argumente RECHTECK, 90 und RICHTUNG zu ermitteln ist.

Argumente werden meist vor ihrer Übergabe an eine Funktion ausgewertet. Das heißt in diesem Beispiel, daß erst nach Auswertung der Symbole RECHTECK und RICHTUNG tatsächlich bekannt ist, welches rechteckige Objekt in welche Richtung ("rechts oder links") zu drehen ist.

Es kommt häufig vor, daß Symbole oder Listen vor einer Auswertung geschützt werden sollen. Das geschieht durch "Verpackung" in einer Liste: (QUOTE ... Symbol / Liste ...).

Die Auswertung geschieht nach dem bekannten Muster - nämlich indem die Funktion QUOTE auf die restlichen Elemente der Liste angewandt wird. Da der Effekt von QUOTE gerade so festgelegt ist, daß diese Funktion ihre Argumente unangetastet läßt, wird das Symbol oder die Liste nicht ausgewertet. Der bequemeren Handhabung halber ist die verkürzte Darstellung: (... Symbol / Liste ...) erlaubt.

Kernpunkt der vorangegangenen Erläuterungen ist der, daß "Lisp" mittels Listen eine uniforme Darstellung von Daten und Operationen erlaubt. Erst durch Betrachtung des ersten Listenelements kann zwischen Daten ("QUOTE ... Daten ... ) und Operationen ( ... Funktion ... oder ... Argumente ...) unterschieden werden.

Diese Uniformität in der Darstellung von Daten und Operationen bringt eine ganze Reihe von Vorteilen mit sich: Sie macht "Lisp" nicht nur zu einer guten Implementierungssprache für Systemprogramme wie etwa Compiler, Interpreter, Binder etc., deren Daten ja häufig Programme sind, sondern sie erleichert auch den Bau speziell auf "Lisp" zugeschnittener Hardware ("Lisp-Maschinen"). Außerdem vereinfacht sie die Darstellung von Wissen, das typischerweise aus operationalen und deskriptiven Bestandteilen zusammengesetzt ist.

"Lisp"-Funktionen werden durch Ausdrücke der Form:

(DEFUN ... Name der Funktion ...

(... Namen der Parameter ...)

... Rumpf der Funktion ...)

vereinbart, zum Beispiel:

(DEFUN DREHE (WAS WIEVIEL WOHIN)

...Rumpf...)

Also findet auch bei der Vereinbarung von Funktionen das Prinzip zur Strukturierung der Programmbausteine in Listenform Anwendung. Wie stets ist das erste Listenelement, hier DEFUN, für die Interpretation der Liste maßgeblich. Die durchgängige Anwendung dieser Regel zum Aufbau von Programmen erleichert das Verständnis der "Lisp"-Semantik. Allerdings leidet die Lesbarkeit unter dem gehäuften Auftreten von Klammern.

Für die Erstellung und Abwicklung von "Lisp"-Programmen ist eine gut ausgestattete Programmierumgebung äußerst wichtig. Tatsächlich stehen solche Umgebungen zur Verfügung; sie enthalten im allgemeinen Werkzeuge wie syntaxgesteuerte Editoren, Interpreter, Compiler, Testhilfen (Tracer, Stepper, Debugger) und Pretty-Printer.

Es ist sicher richtig, daß in der Sprache Lisp die meisten derzeitig existierenden Expertensysteme implementiert sind. Das hat verschiedene Gründe - nicht zuletzt den, daß sie seit etwa 30 Jahren verfügbar ist und sich traditionsgemäß großer Beliebtheit im Bereich der Künstlichen Intelligenz erfreut.

Die Programmiersprache "Smalltalk" folgt dem Paradigma der objektorientierten Programmierung. Dieses Modell geht davon aus, daß bei der Programmierung Vorgänge innerhalb der betrachteten Problemumgebung zu simulieren sind. Das erfordert die möglichst direkte und natürliche Nachbildung von logisch zusammengehörigen Bestandteilen des aktuellen Problems; das Zusammenwirken dieser Bestandteile muß ebenfalls simuliert werden.

"Smalltalk" stellt Objekte und Nachrichten für die Implementierung der genannten Simulationsaufgaben zur Verfügung. Objekte sind Kapseln, die Daten und darauf anwendbare Operationen umschließen. Zugriffe auf den Inhalt von Objekten sind nur über Nachrichten möglich (siehe Abbildung 2). Diese Nachrichten setzen sich zusammen aus der Angabe des Objekts, an das eine Nachricht gerichtet ist ("Empfänger"), einer Operation, die im Empfänger ausgelöst werden soll ("Selektor") sowie der aktuellen Daten, die in die Ausführung der gewünschten Operation einbezogen werden sollen ("Argumente").

Die Erstellung eines "Smalltalk"-Programms umfaßt die Vereinbarung von Objekten und den Versand von Nachrichten zwischen ihnen. Objekte werden nicht einzeln vereinbart; es werden vielmehr zunächst Schablonen für die Objekte erstellt, mit deren Hilfe dann beliebig viele Objekt-Exemplare bequem zu erzeugen sind. In "Smalltalk" werden diese Schablonen als Klassen bezeichnet. An sie können Nachrichten gerichtet werden, die zur Erzeugung von Objekten führen.

Somit zerfällt die Entwicklung von "Smalltalk"-Programmen in drei Phasen: die Vereinbarung von Klassen, die Erzeugung von Objekten als Exemplare von Klassen und den Versand von Nachrichten zwischen den Exemplaren.

Die Vereinbarung von Klassen kann dadurch vereinfacht werden, daß Teile bereits vorhandener Klassen übernommen werden. "Smalltalk" sieht zu diesem Zweck vor, bei der Beschreibung einer neuen Klasse genau eine Oberklasse anzugeben. Andere objektorientierte Programmiersprachen erlauben sogar die Festlegung mehrerer Oberklassen. Auf diese Weise entsteht in "Smalltalk" eine baumartige Hierarchie von Klassen (siehe Abbildung 3). Die Spitze dieses Baumes bilden vordefinierte Systemklassen, unterhalb derer die vom Benutzer vereinbarten Klassen eingeordnet werden.

Suchpfad bei baumartiger Hierarchie eindeutig

Wegen der hierarchischen Anordnung von Klassen löst das Eintreffen einer Nachricht bei einem Objekt eine Suche nach derjenigen Operation aus, die aufgrund des in der Nachricht enthaltenen Selektors anzustoßen ist. Dabei werden nacheinander die Klasse des Empfängerobjekts, ihre Oberklasse, ihre Ober-Oberklasse und so weiter durchsucht. Dieser Suchpfad ist in einer baumartigen Hierarchie eindeutig vorgezeichnet, während in netzwerkartigen Hierarchien die Festlegung einer guten Suchstrategie bedeutend schwieriger ist.

Die Vorzüge einer objektorientierten Sprache wie "Smalltalk" bei der Entwicklung großer Softwaresysteme liegen im Zusammenschluß von Datenbereichen und entsprechenden Zugriffsfunktionen zu eigenständigen Einheiten, in der Absicherung von Betriebsmitteln gegen unerwünschte Verwendung, im hohen Abstraktionsgrad - auf der Ebene der Objekte in Form von Klassen und auf deren Ebene durch hierarchische Anordnung -, in der Gleichbehandlung aller Systemkomponenten als Objekte sowie in der gleichförmigen Auslösung aller Aktivitäten über Nachrichten. Auch hinsichtlich des Baus wissensbasierter Systeme besitzt "Smalltalk" einige angenehme Eigenschaften, welche insbesondere die Darstellung von Wissen betreffen.

Dr. Gerhard Barth ist Professor am Institut für Informatik der Universität Stuttgart.

Der Beitrag basiert auf einem Vortrag, der im Rahmen des CW-CSE-Seminars "Objektorientierte Sprachen für Software-Technologie" in München gehalten wurde.