Einführung in die Programmiermethoden und Sprachen der KI (6): Die Blockwelt

Einleitung

Nach dem Kapitel Grammatiken wird es jetzt wohl Zeit, mal wieder etwas mehr in Richtung LISP zu blicken. Schließlich ist das erste Projekt zur Spracherkennung von Terry Winograd (siehe Literaturverzeichnis am Ende dieses Artikels) in LISP geschrieben. Das hier vorgestellte Programm ist ein kleiner Ausschnitt, der einerseits einen Eindruck von der Blockwelt vermitteln, andererseits aber auch einige wichtige Konzepte der Sprache LISP nahebringen soll. Schwerpunktthemen sind die Property-Liste, das LET-binding und die Schablonenbenutzung.

Die Problemstellung

Vielleicht erinnern Sie sich nicht mehr, aber wahrscheinlich haben auch Sie früher einmal mit Bauklötzen gespielt? Da liegen also die Bauklötze vor Ihnen in irgendeiner Anordnung, z. B. der von Abb 1. Und nun sagt Ihnen jemand: „Pack den roten Würfel auf den blauen.“ Was sich simpel anhört und simpel ist, erfordert dennoch eine stattliche Menge von Information und Organisation. Zunächst müssen die Objekte lokalisiert werden. Anschließend erfolgt die Bewegung in der richtigen Richtung, denn der Befehl „Pack den roten Würfel auf den blauen“ ist nicht zu verwechseln mit „pack den blauen Würfel auf den roten“. Dabei müssen Sie natürlich wissen, daß man auf einer Kugel und einer Pyramide nichts ablegen kann, diese selbst aber sehr wohl auf einen Würfel oder in eine Schachtel transportieren kann. Um die Sache interessant zu machen, sollten Sie nun noch Fragen nach dem Wie, Weshalb und Warum beantworten. Diese Aufgabenstellung erfüllt das in Listing 1 abgedruckte-XLISP Programm, das in Anlehnung an das Programm von Winston und Horn (siehe Literaturverzeichnis) entwickelt wurde. Das letztere benutzt aber die PROCCLAIM-Funktion, die von XLISP leider nicht zur Verfügung gestellt wird. Sie ermöglicht eine dynamische Symbolbindung, entgegen der sonst in LISP üblichen lexikalischen Bindung. Im originalen SHRDLU-Projekt konnten die Befehle in natürlicher Sprache über die Tastatur eingegeben werden. Das würde den Rahmen dieses Aufsatzes natürlich sprengen. Es wird aber versucht, umgangssprachlich klingende Befehle zu verwenden.

Die Propertyliste

Die zentrale Datenstruktur des Blockweltprogramms ist die Propertyliste. Wie der Name schon sagt, handelt es sich um eine Liste, die bestimmte Eigenschaften (engl.: Property) eines Objektes enthält. Schauen wir uns den Aufbau einer solchen Propertyliste am Beispiel der Objekte der Blockwelt einmal näher an; z. B. ist da der rote Würfel. Im ersten Teil des Programms wird dieses Objekt mit Hilfe folgender Anweisungen definiert:

    (putprop 'a 'wuerfel 'ist-ein)
    (putprop 'a 'rot ’farbe)

Putprop ist die Funktion, die eine Eigenschaft in die Propertyliste eines Objektes legt. Das erste Argument ’a ist der Name des Symbols. Unter diesem Namen können wir später auf die Propertyliste zugreifen. Das zweite Argument ’wuerfel bzw. ’rot sind die Werte, die die betreffenden Eigenschaften annehmen. Das dritte Argument der putprop-Funktion schließlich (’ist-ein bzw. ’farbe) ist die Eigenschaft, die man dem Objekt zuspricht. Hier zeigt XLISP eine Abweichung vom Common Lisp Standard, in dem erst die Eigenschaft und dann der Wert angeführt werden. Eine Propertyliste ist in XLISP kein normales Symbol, auf das man wie gewohnt zugreifen könnte. Der einzige in XLISP erlaubte Zugriff auf die Propertyliste erfolgt über speziell dafür konzipierte Funktionen: Putprop und Setf legen eine Eigenschaft auf die entsprechende Propertyliste, Get holt den Wert der Eigenschaft einer Propertyliste, Rem-prop entfernt eine Eigenschaft wieder aus der Propertyliste und Symbol-

plist gibt die ganze Propertyliste zurück. Es sei bemerkt, daß Putprop und Setf in Bezug auf die Propertylisten gleiche Wirkung zeigen. Die Anwendung der Putprop-Funktion beschränkt sich aber auf die Anwendung auf Propertylisten, während Setf (Abkürzung für Set Field) auch dazu benutzt werden kann, den car einer Liste, den cdr einer Liste, das n.te Element eines Arrays etc. zu setzen. Abb. 2 zeigt den Aufruf und die Wirkung der entsprechenden XLISP-Funktionen auf die Abbildung 2: Protokoll von Abfragen der Propertyliste des Symbols ’a. Propertyliste des Objektes ’a, wie sie vom Programm Listing 1 erzeugt wird.

Abb. 3 faßt die Syntax der obigen Funktionen in XLISP zusammen.

Der Vollständigkeit halber sei noch bemerkt, daß in Common Lisp und XLISP eine der Propertyliste ähnliche Datenstruktur existiert, die aber in dem obigen Programm nicht verwendet wird: Die Assoziativliste. Hier werden allerdings die Eigenschaft und ihr Wert in einer 2-elementigen Subliste zusammengefaßt. Die Assoziativliste des Objekts ’a würde dann also lauten:

    ((IST-EIN WUERFEL) (GETRAGEN-YON TISCH) (TRAEGT-DIREKT B) FARBE ROT))

Der Zugriff auf diese Liste erfolgt mit Hilfe der Assoc-Funktion.

Das LET-Binding

Das LET-Binding gehört in die Gruppe der Kontrollstrukturen, die XLISP zur Verfügung stellt (siehe auch Teil 2 dieser Serie). Um die Wirkung dieser Kontrollstruktur besser zu verstehen, ist es vielleicht sinnvoll zunächst die Begriffe freie Variable und gebundene Variable zu erläutern. Pascal-Programmierer kennen die Begriffe globale Variable und lokale Variable. Die beiden Begriffstypen stimmen weitgehend überein, nur daß eben in LISP nicht die formalen Typenbeschränkungen von Pascal existieren. Schauen wir uns als Beispiel für die Wirkung der LET-Bindung die Funktion pack-auf mit der zugehörigen Funktion initialisierung aus der Blockwelt an: (siehe Beispiel 1).

Die Funktion initialisierung enthält keine Parameterliste und keine LET-Bindung. Folglich sind alle in dieser Funktion benutzten Variablen frei, d.h. sie existieren noch, nachdem die

(putprop <Symbol) (Wert) (Eigenschaft))

Legt eine Eigenschaft auf die Propertyliste des Symbols. Ist die Eigenschaft bereits auf der Property liste, wird der alte Wert überschrieben. Gibt den Wert zurück an das Programm.

: Das Symbol, auf dessen Propertyliste die Eigenschaft gelegt werden soll.

: Der Wert den die Eigenschaft annehmen soll.

: Die Eigenschaft, die auf die Propertyliste des Symbols gesetzt werden soll.

(get )

Holt den Wert der Eigenschaft eines Symbols aus der Propertyliste.

Gibt den Wert der Eigenschaft zurück oder NIL, wenn die Eigenschaft nicht vorhanden ist.

: Das Symbol, von dem der Wert einer Eigenschaft geholt werden soll.

: Die Eigenschaft, dessen Wert von der Propertyliste des Symbols geholt werden soll.

(renprop )

Ritfernt eine Eigenschaft aus der Property liste des Symbols. Gibt immer NIL zurück.

: Das Symbol, von dem die Eigenschaft aus der Property liste entfernt werden soll.

: Die Eigenschaft, die entfernt werden soll.

(symbol-plist )

Gibt die gesamte Property liste des Symbols zurück.

: Das Symbol, dessen Propertyliste zurückgegeben werden soll.

(setf )

Setzt den Wert eines Objektes. Gibt den Wert zurück. Wirkt wie putprop, wenn das Objekt die Eigenschaft aus der Propertyliste eines Symbols ist. Die Anwendung dieser sehr mächtigen Funktion ist nicht auf Propertylisten beschränkt.

Das Objekt, dessen Wert gesetzt werden soll.

Bei dem Objekt kann es sich m folgende Datenstrukturen handeln:

: Setzt den Wert eines Symbols.

(car ): Setzt den Wert des ersten Elementes einer Liste.

(cdr ): Ersetzt die Restliste einer Liste durch den Wert. Dieser muß natürlich eine Liste sein.

(nth ):

Setzt den Wert des n.-ten Elementes der Liste.

(aref ):

Setzt den Wert des n.-ten Elementes eines Arrays.

(get ):

Setzt den Wert der Eigenschaft des Symbols.

(symbol-value ):

Setzt den Wert der Eigenschaft eines Symbols.

(symbol-plist ):

Setzt die gesamte Propertyliste eines Symbols. Wert miß dann natürlich die Propertyliste

Abbildung 3: Syntax der Zugriffsoperationen auf Propertylisten in XLISP.

Funktion verlassen wurde. Damit steht der Aktionsbaum, dessen Wurzeln mit der Funktion initialisierung gelegt wurde, für spätere Anfragen zur Verfügung. Im allgemeinen ist dieser Nebeneffekt freier Variablen unerwünscht, weil die Werte freier Variablen in allen Programmteilen verändert werden können, was schnell zu schwer entwanzbaren Programmabstürzen führt. Die Funktion pack-auf enthält die Parameterliste mit den gebundenen Variablen objekt und traegt, stellvertretend für das zu bewegende objekt und das Objekt, das es zukünftig tragen soll (traegt). In den ersten drei Zeilen des Programms werden mögliche Fehler abgefangen bzw. die Initialisierung des Aktionsbaums und des Plans vorgenommen, d. h. die freien Variablen des Programms werden initialisiert. Das folgende LET-Binding umschließt den wesentlichen Teil der Funktion pack-auf. Alle im Bindungsteil (unterstrichen) initialisierten Variablen sind an die LET-Funktion gebunden. Dies sind die Variablen objektl und traegtl. Sie werden initialisiert mit den Werten von objekt bzw. traegt. Gleich darauf folgen zwei Zeilen, indem diese Variablen mit setq verändert werden, wenn die entsprechende Bedingung der cond-Funktion erfüllt ist. Damit ändert sich aber nichts an der Bindung dieser Variablen! Diese Zeilen ermöglichen es dem Benutzer, ein verschlüsseltes Objekt wie z. B. (wuerfel rot), also einen roten Würfel anzugeben. Eine Variante der LET-Bindung ist in der Funktion traegt-zusaetzlich zu finden: (Siehe Beispiel 2).

Hier wird in der dritten Zeile des Initialisierungsteils (unterstrichen) die gebundene Variable traegt mit einem Wert initialisiert, der den Wert der ebenfalls gebundenen Variablen nachfolgend als bereits vorhanden vorraussetzt. Falls Sie sich noch an Teil 2 dieser Serie erinnern, wissen Sie, daß das normalerweise nicht erlaubt ist, weil die Initialisierung der gebundenen Variablen parallel erfolgt; d. h. es werden erst alle Werte berechnet und dann die Variablen initialisiert. Das würde im Fall der Funktion traegt-zusaetzlich zu der Fehlermeldung führen:

Unbound Variable nachfolger 1

Daß die Funktion dennoch funktioniert, dafür sorgt das ★ hinter dem LET! let * definiert somit die sequentielle Initialisierung der gebundenen Variablen. Also wird erst die Variable knoten1 initialisiert, dann die Variable nachfolger1 und zum Schluß (mit nunmehr initialisierten gebundenen Variablen) traegt.

(defun initialisierung () Beispiel 1 (setq plan nil) (setq knoten 'geschichte) (setq nachfolger (gensym)) (anfuegen nachfolger knoten)) (defun pack-auf (objekt traegt) (cond ((equal objekt traegt) (break "Fehler: " (,objekt kann nicht auf sich selbst gepackt werden)))) (initialisierung) (let ((objekt1 objekt) (traegt1 traegt)) (cond ((listp objekt) (setq objekt1 (finde-objekt objekt objektliste)))) (cond ((listp traegt) (setq traegt1 (finde-objekt traegt objektliste)))) (putprop nachfolger (list 'pack-auf objekt traegt) Situation) (cond ((equal (get objekt1 'getragen-von) traegt1) (break "Fehler:" (,traegt1 traegt bereits .objektl)))) (pack-hin objekt1 (mach-platz objekt1 traegtl nachfolger) nachfolger) (pp (reverse plan))))

(defun traegt-zusaetzlich (objekt ort &optional nachfolger2) (cond ((equal nil nachfolger2) (initialisierung) (cond ((listp objekt) (setq objekt (finde-objekt objekt objektliste)))) (setq nachfolger2 nachfolger))) (let * ((knoten1 nachfolger2) (nachfolger1 (gensym)) (traegt (hole-objekt-unter ort nachfolger1))) ...

Beispiel 2

Manipulation in der Blockwelt

Abbildung 1: Blockwelt in Listing 1.

(ausgabe (pack-auf 'a 'c))

((GREIFE D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (LASS-LOS D) (GREIFE B) (BEWEGE-OBJEKT B (RAUM UEBER TISCH FUER B)) (LASS-LOS B) (GREIFE A) (BEWEGE-OBJEKT A (RAUM UEBER C FUER A)) (LASS-LOS A) ) T >

Beispiel 3

In der Blockwelt stehen nur ganz einfache Manipulationen zur Verfügung. Man kann einen Klotz greifen und ihn irgendwo hinlegen. In Terry Winograds Originalprogramm stehen noch komplizierte Programmteile zur Verfügung, die auch umgangssprachliche Anweisungen wie pack den roten Klotz auf den blauen verkraften (siehe hierzu auch den Übersichtsartikel vom gleichen Verfasser). In unserem simplen Programm muß das Objekt entweder bei seinem Symbolnamen genannt werden (also a, b, c, d, e oder f) oder als Liste mit den Eigenschaften ist-ein und farbe. Um den komplizierten Parser für die Umgangssprache zu vermeiden, wurden die Befehle umgangssprachlich ähnlich aufgebaut. So lautet also der obige Befehl für unsere Blockwelt (pack-auf ’a ’c) oder (pack-auf '(wuerfel rot) ’(wuerfel blau)). Das Programm versucht dann unseren Befehl auszuführen und merkt sich alle auszuführenden Arbeiten in der freien Variable plan, die zu Beginn des Programms auf NIL gesetzt wird. Sowohl die greifende Hand als auch die einzelnen Blöcke sind als freie Propertylisten definiert. Alle Funktionen geben den plan in umgekehrter Reihenfolge zurück. Um den Plan besser lesbar zu machen, ist die Funktion ausgabe zugefügt worden, die die Liste plan in richtiger Reihenfolge ausgibt. Bevor wir uns der Analyse des Programms zuwenden, schauen wir uns die Abarbeitung des obigen Befehls einmal an: (Siehe Beispiel 3).

Richtig, wie wir Abb.1 entnehmen, liegt ja auf dem roten Klotz a noch ein gelber Klotz b und auf dem Würfel c liegt noch die Pyramide d. Also muß erst die Pyramide d von Klotz c entfernt werden, dann muß Klotz b von Klotz a entfernt werden und dann erst kann Würfel a auf Würfel c gelegt werden. Gut. Nun versuchen wir, den Würfel b auf die Kugel zu legen: (Siehe Beispiel 4).

Kein Problem, wie man sieht.

Natürlich ist das Programm stark verbesserungsfähig. Man könnte z. B. noch Koordinaten auf die Propertyliste der Objekte setzen und bei den entsprechenden Manipulationen berücksichtigen. Das wäre ja bei einem realen Roboterarm unbedingt erforderlich. In diesem Programm wird die Koordinatenangabe simuliert über die Mitteilung (RAUM UEBER _ FUER _). Es fehlt auch ein Mechanismus, der erkennt, wenn die Beziehungen nicht eindeutig sind und mit einer Rückfrage reagiert. Also wenn z. B. zwei rote Würfel existieren, müßte das Programm auf den Befehl (pack-auf '(wuerfel rot) '(schachtel gruen)) mit der Rückfrage reagieren: Ich weiß nicht welchen roten Würfel du meinst!

Beispiel 4

(ausgabe (pack-auf ’b ’e)) (BREAK: TRAEGT-ZUSAETZLICH KANN NICHT DAS TRAGEN ARRANGIEREN)

Stimmt auffallend, auf eine Kugel paßt kein Würfel. Und was ist, wenn die Kugel in die Schachtel soll?

(ausgabe (pack-auf ’(kugel gelb) '(schachtel gruen))) ((GREIFE E) (BEWEGE-OBJEKT E (RAUM UEBER F FUER E)) (LASS-LOS E) ) T

Analyse des Blockweltprogramms

(trace 'initialisierung) (trace 'pack-auf) (trace 'finde-objekt) (trace 'mach-platz) (trace 'pack-hin) (trace 'greife) (trace 'bewege-objekt) (trace 'bewege-hand) (trace 'lass-los) (trace 'werde-los) (trace 'oben-frei) (trace 'entferne-traegt) (trace 'traegt-zusaetzlich) (trace 'mach-raum) (trace 'finde-raum) (trace 'hole-objekt-unter) (trace 'oben-auf) (trace 'neu-oben-auf) (trace 'anfuegen)

Abbildung 4: Trace setzen zur Verfolgung der Arbeit im Blockweltprogramm.

Ich will mich an dieser Stelle nicht zu breit auslassen, weil der interessierte Leser am besten mit dem Programm an seinem XLISP-Interpreter herumspielt. Um die Funktionsweise besser zu verstehen, sollte man alle Funktionen einmal in die Traceliste aufnehmen. Abb. 4 zeigt die entsprechenden Vereinbarungen, die zu Beginn des Programms aufgenommen werden können. Abb. 5 zeigt die entsprechende Ausgabe. Als Hilfe zeigt Abb. 6 eine Übersicht über die Aufgaben der einzelnen Funktionen. Zu erwähnen ist noch das Schlüsselwort &optional. Ab diesem Schlüsselwort folgen in der Parameterliste (wer hätte das gedacht?) Parameter, die weggelassen werden können, ohne daß der XLISP-Interpreter einen TOO FEW ARGUMENTS-Fehler meldet. Die fehlenden Variablen werden NIL gesetzt, ohne daß die Bindung an die Funktion verlorengeht. In unserem Programm wird diese Eigenschaft genutzt, um z. B. das Greifen eines Blocks alleine durchzuführen. Normalerweise wird ja die Funktion greife nur als Folge eines übergeordneten Befehls (wie z. B. (pack-auf ’a ’c)) ausgeführt. Dann wird aber der Nachfolger des Aktionsbaumes übergeben und das neue Element an diesen Baum angehängt. Man kann unter Benutzung des &optional-Schlüsselwortes auch die Hand direkt einen Klotz greifen lassen: (greife ’a). Der fehlende Parameter nachfolger2 wird selbständig NIL gesetzt und im ersten Teil der Funktion wird die notwendige Initialisierung des Baumes vorgenommen.

T

(pack-auf 'a ’c)

(PACK-AUF (QUOTE A) (QUOTE C)) (INITIALISIERUNG) (ANFUEGEN NACHFOLGER KNOTEN) <<< (G1) <<< (G1) (PACK-HIN OBJEKT (MACHT-PLATZ OBJEKT TRAEGT NACHFOLGER) NACHFOLGER) (MACH-PLATZ OBJEKT TRAEGT NACHFOLGER) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G2) (FINDE-RAUM OBJEKT TRAEGT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G3) <<< NIL (MACH-RAUM OBJEKT TRAEGT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G3 G4) (WERDE-LOS (CAR DRUEBER) NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G5) (PACK-HIN OBJEKT (FINDE-RAUM OBJEKT (QUOTE TISCH) NACHFOLGER1) NACHFOLGER1) (FINDE-RAUM OBJEKT (QUOTE TISCH) NACHFOLGEND (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G6) <<< (RAUM UEBFR TISCH FUER D) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G6 G7) (GREIFE OBJEKT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G8) (BEWEGE-HAND (OBEN-AUF OBJEKT NACHFOLGER1) NACHFOLGER1) (OBEN-AUF OBJEKT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTÖG) <<< (G9) <<< (OBEN-AUF D) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G9 G10) <<< (OBEN-AUF D) <<< ((GREIFE D)) (BEWEGE-OBJEKT OBJEKT ORT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G8 G11) (ENTFERNE-TRAEGT OBJEKT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G12) <<< NIL (BEWEGE-HAND (NEU-OBEN-AUF OBJEKT NEUORT NACHFOLGER1) NACHFOLGER1) >>> (NEU-OBEN-AUF OBJEKT NEUORT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G12 G13) <<< (NEU-OBEN-AUF D (RAUM UEBER TISCH FUER D)) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G12 G13 G14) <<< NEU-OBEN-AUF D (RAUM UEBER TISCH FUER )) (TRAEGT-ZUSAETZLICH OBJEKT NEUORT NACHFOLGER1) (HOLE-OBJEKT-UNTER ORT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G16) <<< TISCH (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G12 G13 G14 G15) <<< TISCH <<< ((BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (GREIFE D)) (LASS-LOS OBJEKT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G8 G11 G17) <<< ((LASS-LOS D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (GREIFE D)) <<< ((LASS-LOS D) (BEWEGE-OBJEKT D (RAUM UFBER TISCH FUER D)) (GREIFE D)) <<< ((LASS-LOS D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (GREIFE D)) (FINDE-RAUM OBJEKT TRAEGT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G5 G18) <<< (RAUM UEBER C FUER A) <<< (RAUM UEBER C FUER A) <<< (RAUM UEBER C FUER A) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G2 G19) (GREIFE OBJEKT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G20) (OBEN-FREI DRUEBER OBJEKT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G21) (WERDE-LOS (CAR DRUEBER) NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G22) (PACK-HIN OBJEKT (FINDE-RAUM OBJEKT (QUOTE TISCH) NACHFOLGEND NACHFOLGER1) (FINDE-RAUM OBJEKT (QUOTE TISCH) NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G23) <<< (RAUM UEBER TISCH FUER B) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G23 G24) (GREIFE OBJEKT NNACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G25) (BEWEGE-HAND (OBEN-AUF OBJEKT NACHFOLGER1) NACHFOLGER1) (OBEN-AUF OBJEKT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G26) <<< (OBEN-AUF B) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G26 G27) <<< (OBEN-AUF B) <<< ((GREIFE B) (LASS-LOS D) (BEWEGE-OBJEKT D (RAUM TISCH WER DI) (GREIFE D)) (BEWEGE-OBJEKT ORT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G25 G28) (ENTFERNE-TRAEGT OBJEKT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G29) <<< NIL (BEWEGE-HAND (NEU-OBEN-AUF OBJEKT NEUORT NACHFOLGER1) NACHFOLGER1) (NEU-OBEN-AUF OBJEKT NEUORT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G29 G30) <<< (NEU-OBEN-AUF B (RAUM UEBER TISCH FUER B)) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G29 G3C G31) <<< (NEU-OBEN-AUF B (RAUM UEBER TISCH FUER B)) (TRAEGT-ZUSAETZLICH OBJEKT NEUORT NACHFOLGER1) (HOLE-OBJEKT-UNTER ORT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G33) <<< TISCH (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G29 G30 G31 G32) <<< TISCH <<< ((BEWEGE-OBJEKT B (RAUM UEBER TISCH EUER B)) (GREIFE B) (LASS-LOS D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (GREIFE D)) (LASS-LOS OBJEKT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G25 G28 G34) <<< ((LASS—LOS B) (BEWEGE-OBJEKT B (RAUM UEBER TISCH FUER B)) (GREIFE B) (LASS-LOS D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (GREIFE D)) <<< ((LASS-LOS B) (BEWEGE-OBJEKT B (RAUM UEBER TISCH FUER B)) (GREIFE B) (LASS-LOS D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (GREIFE D)) <<< ((LASS-LOS B) (BEWEGE-OBJEKT B (RAUM UEBER TISCH FUER B)) (GREIFE B) (LASS-LOS D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (GREIFE D)) <<< NIL (BEWEGE-HAND (OBEN-AUF OBJEKT NACHFOLGER1) NACHFOLGER1) (OBEN-AUF OBJEKT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G21 G35) <<< (OBEN-AUF A) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G21 G35 G36) <<< (OBEN-AUF A) <<< ((GREIFE A) (LASS-LOS B) (BEWEGE-OBJEKT B (RAUM UEBER TISCH FUER B)) (GREIFE B) (LASS-LOS D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (GREIFE D)) (BEWEGE-OBJEKT OBJEKT ORT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G20 G37) (ENTFERNE-TRAEGT OBJEKT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G38) <<< NIL (BEWEGE-HAND (NEU-OBEN-AUF OBJEKT NEUORT NACHFOLGER1) NACHFOLGER1) (NEU-OBEN-AUF OBJEKT NEUORT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G38 G39) <<< (NEU-OBEN-AUF A (RAUM UEBER C FUER A)) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G38 G39 G40) <<< (NEU-OBEN-AUF A (RAUM UEBER C FUER A)) (TRAEGT-ZUSAETZLICH OBJEKT NEUORT NACHFOLGER1) (HOLE-OBJEKT-CUNTER ORT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G42) <<< C (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G38 G39 G40 G41) <<< C <<< ((BEWEGE-OBJEKT A (RAUM UEBER C EUER A)) (GREIFE A) (LASS-LOS B) (BEWEGE-OBJEKT B (RAUM UEBER TISCH FUER B)) (GREIFE B) (LASS-LOS D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (GREIFE D)) (LASS-LOS OBJEKT NACHFOLGER1) (ANFUEGEN NACHFOLGER1 KNOTEN1) <<< (G20 G37 G43) <<< ((LASS-LOS A) (BEWEGE-OBJEKT A (RAUM UEBER C FUER A)) (GREIFE A) (LASS-LOS B) (BEWEGE-OBJEKT B (RAUM UEBER TISCH FUER B)) (GREIFE B) (LASS-LOS D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (GREIFE D)) <<< ((LASS-LOS A) (BEWEGE-OBJEKT A (RAUM UBER C EUER A)) (GREIFE A) (LASS-LOS B) (BEWEGE-OBJEKT B (RAUM UEBER TISCH FUER B)) (GREIFE B) (LASS-LOS D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (GREIFE D)) <<< ((LASS-LOS A) (BEWEGE-OBJEKT A (RAUM UEBER C FUER A)) (GREIFE A) (LASS-LOS B) (BEWEGE-OBJEKT B (RAUM UEBER TISCH FUER B)) (GREIFE B) (LASS-LOS D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (GREIFE D)) ((LASS-LOS A) (BEWEGE-OBJEKT A (RAUM UEBER C FUER A)) (GREIFE A) (LASS-LOS B) (BEWEGE-OBJEKT B (RAUM UEBER TISCH FUER B)) (GREIFE B) (LASS-LOS D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (GREIFE D))

(ausgabe plan) ((GREIFE D) (BEWEGE-OBJEKT D (RAUM UEBER TISCH FUER D)) (LASS-LOS D) (GREIFE B)

Der Aktionsbaum

In der Initialisierungsfunktion wird die Wurzel eines Aktionsbaumes gelegt (knoten genannt), der es ermöglicht, nach Ablauf einer Aktion Fragen nach den Wie, Weshalb und Warum zu beantworten. Dazu wird jede Funktion mit dem optionalen Parameter nachfolger aufgerufen. Dieser wird an die Variable knoten 1 lokal gebunden, gleichzeitig wird ein neuer Knoten (nachfolger1 genannt) fhit Hilfe der Funktion gensym erzeugt. In den jeweils aktuellen Knoten wird dann die in der jeweiligen Funktion ablaufende Aktion abgespeichert. Abb. 8 zeigt das Listing der Funktion, mit deren Hilfe man den Aktionsbaum auslisten kann.

Die zugehörige Ausgabe für die Aktion (pack-auf ’a ’c) findet man in Abb. 7. Die Verbindungslinien wurden allerdings mit Hand eingetragen.

In diesem Aktionsbaum suchen die Funktionen sag-wie, sag-weshalb und sag-warum nach den entsprechenden Antworten. Die Funktion sag-wie soll die Antwort auf die Methode finden, wie das gesteckte Ziel erreicht werden kann. Sie muß also im Aktionsbaum nach dem Ziel als Schlüssel suchen und die Nachfolger dieses Schlüssels ausgeben. Hierzu wird einfach rekursiv der Aktionsbaum abgetastet und entweder bei Auffinden des Schlüssels die Nachfolger ausgegeben oder bei Fehlen des Schlüssels NIL ausgegeben. Zur Ausgabe der Nachfolger noch ein Wort: Hier findet die Funktion mapcar Anwendung. Als erstes Argument verlangt diese Funktion eine Funktionsdefinition. Als zweites Argument muß eine Liste pro Argument der Funktionsdefinition folgen. Die Funktion mapcar holt sich dann aus jeder Liste das erste Element und übergibt es an die Parameterliste der Funktionsdefinition. Der Vorgang wird dann mit dem cdr der Listen wiederholt, bis die Listen leer sind. Die Antwort auf die Frage weshalb wird von der Funktion sag-weshalb ausgegeben. Hierzu muß zunächst wieder der Schlüssel gesucht werden und anschließend die Vorgängerliste ausgegeben werden. Die Frage nach dem Warum ist besonders einfach zu beantworten. Hierzu ist lediglich die Wurzel des Baumes auszugeben.

Ausblick

Die Serie geht langsam ihrem Ende zu. Ich möchte Ihnen aber in den verbleibenden beiden Ausgaben zwei Verfahren zur Wissensspeicherung erläutern: Die Rahmentechnologie und das Matrixkonzept.

Dr. Sarnow

Literatur:

[1] Sarnow, K.: Künstliche Intelligenz. Einführung. ST-Computer, Heft 11/86.

[2] Winston, P.H. und B. Horn: LISP. Addison-Wesley Publishing, Rea-ding Massachusetts, 1984.

[3] Winograd, Terry: Understanding Natural Language. New York 1972.

(Fortsetzung von S. 34) (BEWEGE-OBJEKT B (RAUM UEBER TISCH FUER B)) (LASS-LOS B) (GREIFE A)
(BEWEGE-OBJEKT A (RAUM UEBER C FUER A)) (LASS-LOS A) ) T

(exit)

Abbildung 5: Trace der Blockwelt beim Abarbeiten von (pack-auf ’a ’c).

Name der Funktion | Beschreibung der Aufgabe PACK-AUF | Legt ein Objekt auf ein anderes ab. Die zentrale Funktion des Blockwelt Programms. Alle anderen Funktionen sind »ehr oder weniger Hilfsfiaikticnen dieser Funktion. PACK-HIN | Unterscheidet sich von PACK-AUF, indem das Objekt an eine freie Stelle gelegt wird. MACH-PLATZ | Beseitigt alle Objekte, die der durchzuführenden Aktien im Wege sind. FINDE-RAUM | Versucht an der gewünschten Stelle einen leeren Raun zu finden. MACH-RAUM | Räumt die gewünschte Stelle leer. WERDE-LOS | Legt die Objekte, die der Aktion ist Wege sind auf dem Tisch ab. HOLE-OBJEKT-UNTER | Ermittelt die Liste der Objekte, die sich unter einem Objekt befinden.

Abbildung 6: Übersicht über die wichtigsten Funktionen der Blockwelt. Die meisten Namen sind selbsterklärend.

Abbildung 7

Abbildung 8: Funktion zum Ausdrucken des Aktionsbaumes.

(putprop ’hand NIL 'greifend) ;Die Hand greift nichts

(putprop ’a 'wuerfel 'ist-ein) ;Es folgt die Definition

(putprop 'a 'tisch 'getragen-von) ;der Objekte der Blockwelt.

(putprop 'a '(b) ’traegt-direkt) ;Objekt a ist also ein roter

(putprop ’a 'rot 'farbe) ;Würfel der auf dem Tisch ;liegt und Objekt b trägt.

(putprop 'b 'wuerfel 'ist-ein) (putprop 'b 'a 'getragen-von) (putprop 'b '() 'traegt-direkt) (putprop 'b 'gelb 'farbe)

(putprop 'c 'wuerfel 'ist-ein) (putprop 'c 'tisch 'getragen-vcn) (putprop 'c '(d) 'traegt-direkt) (putprop 'c 'blau 'farbe)

(putprop 'd 'pyramide 'ist-ein) (putprop 'd 'c 'getragen-von) (putprop 'd '() 'traegt-direkt) (putprop 'd 'gruen 'farbe)

(putprop 'e 'kugel 'ist-ein) (putprop 'e 'tisch 'getragen-vcn) (putprop 'e '() 'traegt-direkt) (putprop 'e 'gelb 'farbe)

(putprop 'f 'schachtel 'ist-ein) (putprop 'f 'tisch 'getragen-vcn) (putprop 'f '() 'traegt-direkt) (putprop 'f 'gruen 'farbe)

(setq Objektliste '(a b c d e f))

(defun initialisierung () (setq plan nil) (setq knoten 'geschichte) (setq nachfolger (gensym)) (anfuegen nachfolger knoten))

(defun pack-auf (objekt traegt) (cond ((equal objekt traegt) (break "Fehler: " * (.objekt kann nicht auf sich seihst gepackt werden)))) (initialisierung) (cond ((listp objekt) (setq objekt (finde-objekt objekt objektliste)))) (cond ((listp traegt) (setq traegt (finde-objekt traegt objektliste)))) (putprop nachfolger (list 'pack-auf objekt traegt) 'situation) (cond ((equal (get objekt 'getragen-von) traegt) (break "Fehler:" '(.traegt traegt bereits .objekt)))) (pack-hin objekt (mach-platz objekt traegt nachfolger) nachfolger))

(defun finde-objekt (objekt objektliste) (cond ((equal objekt liste nil) (break "Objekt nicht in Objektliste:" objekt)))

(cond ((equal (get (car objektliste) 'ist-ein) (car objekt))
    (cond ((equal (get (car objektliste) 'farbe) (cadr objekt))
                (car objektliste))
            (t (finde-objekt objekt (cdr objektliste)))))
        (t (finde-objekt objekt (cdr objektliste)))))

(defun mach-platz (objekt traegt optional nachfolger2) (cond ((equal objekt traegt) (break "Fehler: " '(,objekt kann nicht fuer sich selbst platz machen)))) (coud ((equal nil nachfolger2) (initialisierung) (cond ((listp objekt) (setq objekt (finde-objekt objekt objektliste)))) (cond ((listp traegt) (setq traegt (finde-objekt traegt objektliste)))) (setq nachfolger2 nachfolger)))

(let ((knoten1 nachfolger2)
    (nachfolger1 (gensym)))
    (anfuegen nachfolger1 knoten1)
    (putprop nachfolger1 (list 'nach-platz objekt traegt) 'situation)
    (cond ((finde-ram objekt traegt nachfolger1))
        ((mach-raun Objekt traegt nachfolger1)))))

(defun pack-hin (objekt ort &optional nachfolger2) (cond ((equal nil nachfolger2) (initialisierung) (cond ((listp objekt) (setq objekt (finde-objekt objekt objektliste)))) (setq nachfolger2 nachfolger))) (let ((knoten1 nachfolger2) (nachfolger1 (gensym))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list 'pack-hin objekt ort) 'situation) (greife objekt nachfolger1) (bewege-objekt objekt ort nachfolger1) (lass-los objekt nachfolger1)))

(defun greife (objekt &optional nachfolger2) (cond ((not (equal (get 'hand 'greifend) nil)) (break "Fehler: Band greift bereits " (get 'hand 'greifend)))) (cond ((equal nil nachfolger2) (initialisierung) (cond ((listp objekt) (setq objekt (finde-objekt objekt objektliste)))) (setq nachfolger2 nachfolger))) (let ((darueber (get 'hand 'greifend)) (drueber (get objekt 'traegt-direkt)) (knoten1 nachfolger2) (nachfolger1 (gensym))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list 'greife objekt) 'situation) (cond (darueber (werde-los darueber nachfolger1))) (cond (drueber (oben-frei drueber objekt nachfolger1))) (bewege-hand (cben-auf objekt nachfolger1) nachfolger1) (setf (get 'hand 'greifend) objekt) (setq plan (cons (list 'greife objekt) plan))))

(defun bewege-objekt (objekt neuort «optional nachfolger2) (cond ((equal nil nachfolger2) (initialisierung) (cond ((listp objekt) (setq objekt (finde-objekt objekt objektliste)))) (setq nachfolger2 nachfolger))) (let ((knoten1 nachfolger2) (nachfolger1 (gensym))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list 'bewege-objekt objekt neuert) 'situation) (entferne-traegt objekt nachfolger1) (bewege-hand (neu-oben-auf objekt neuort nachfolger1) nachfolger1) (setf (get objekt 'position) neuort) (traegt-zusaetzlich objekt neuort nachfolger1) (setq plan (cons (list 'bewege-objekt objekt neuort) plan))))

(defun bewege-hand (position &optional nachfolger2) (cond ((equal nil nachfolger2) (initialisierung) (cond ((listp objekt) (setq objekt (finde-objekt objekt objektliste)))) (setq nachfolger2 nachfolger))) (let ((knoten1 nachfolger2) (nachfolger1 (gensym))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list 'bewege-hand Position) 'situation) (setf (get 'hand 'position) Position)))

(defun lass-los (objekt &optional nachfolger2) (cond ((equal (get 'hand 'greifend) nil) (break "Fehler: " * (hand greift nichts)))) (cond ((equal nil nachfolger2) (initialisierung) (cond ((listp objekt) (setq Objekt (finde-objekt Objekt objektliste)))) (setq nachfolger2 nachfolger))) (let ((knoten1 nachfolger2) (nachfolger1 (gensym))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list 'lass-los objekt) 'situation) (cond ((not (get objekt 'getragen-von)) nil) (T (setf (get 'hand 'greifend) nil) (setq plan (cons (list 'lass-los objekt) plan))))))

(defun werde-los (objekt &optional nachfolger2) (cond ((equal nil nachfolger2) (initialisierung) (ocod ((listp objekt) (setq objekt (finde-objekt objekt objektliste)))) (setq nachfolger2 nachfolger))) (let ((knoten1 nachf olger2) (nachfolger1 (gensym))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list 'werde-los objekt) 'situation) (pack-hin objekt (finde-raum objekt 'tisch nachfolger1) nachfolger1)))

(defun oben-frei (drueber objekt &optional nachfolger2) (cond ((equal nil nachfolger2) (initialisierung) (cond ((listp objekt) (setq objekt (finde-objekt objekt objektliste)))) (setq nachfolger2 nachfolger))) (let ((knoten1 nachfolger2) (nachfolger1 (gensym))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list 'oben-frei drueber objekt) 'situation) (do ((drueber drueber (edr drueber))) ((null drueber)) (werde-los (car drueber) nachfolger1))))

(defun entferne-traegt (objekt &optional nachfolger2) (cond ((equal nil nachfolger2) (initialisierung) (cond ((listp objekt) (setq objekt (finde-objekt objekt objektliste)))) (setq nachfolger2 nachfolger))) (let ((traegt (get objekt 'getragen-von)) (knoten1 nachfolger2) (nachfolger1 (gensym))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list 'entferne-traegt objekt) 'situation) (setf (get traegt 'traegt-direkt) (remove objekt (get traegt 'traegt-direkt))) (setf (get objekt 'getragen-von) nil)))

(defun traegt-zusaetzlich (objekt ort &optional nachfolger2) (cond ((equal nil nachfolger2) (initialisierung) (cond ((listp objekt) (setq objekt (finde-objekt objekt objektliste)))) (setq nachfolger2 nachfolger))) (let* ((knoten1 nachfolger2) (nachfolger1 (gensym)) (traegt (hole-objekt-unter ort nachfolger1))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list ' traegt-zusaetzlich objekt ort) 'situation) (cond ((or (equal traegt 'tisch) (equal (get traegt 'ist-ein) 'schachtel) (equal (get traegt 'ist-ein) 'wuerfel)) (setf (get traegt 'traegt-direkt) (cons objekt (get traegt 'traegt-direkt))) (setf (get objekt 'getragen-von) traegt)) (t (break '(traegt-zusaetzlich kann nicht das tragen arrangieren))))))

(defun mach-raum (objekt traegt &optional nachfolger2) (cond ((equal objekt traegt) (break "Fehler: " '(,objekt kann nicht fuer sich selbst raum machen)))) (cond ((equal nil nachfolger2) (initialisierung) (cond ((listp objekt) (setq objekt (finde-objekt objekt objekt liste)))) (setq nachfolger2 nachfolger))) (let ((knoten1 nachfolger2) (nachfolger1 (gensym))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list 'mach-raum objekt traegt) 'situation) (do ((drueber (get traegt 'traegt-direkt) (cdr drueber))

    (ort nil))
            (ort ort)
    (werde-loa (car drueber) nachfolger1)
    (setq ort (finde-raum objekt traegt nachfolger1)))))

(defun finde-raum (objekt traegt &optional nachfolger2) (cond ((equal objekt traegt) (break "Fehler: " '(,objekt kann nicht fuer sich selbst raum finden)))) (cond ((equal nil nachfolger2) (initialisierung) (cond ((listp objekt) (setq objekt (finde-objekt objekt objektliste)))) (cond ((listp traegt) (setq traegt (finde-objekt traegt objektliste)))' (setq nachfolger2 nachfolger))) (let ((darueber (get traegt 'traegt-direkt)) (knoten1 nachfolger2) (nachfolger1 (gensym))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list 'finde-raum objekt traegt) 'situation) (cond ((or (equal traegt 'tisch) (null darueber)) '(raum ueber ,traegt fuer ,objekt)) (T nil))))

(defun hole-objekt-unter (ort &optional nachfolger2) (cond ((equal nil nachfolger2) (Initialisierung) (setq nachfolger2 nachfolger))) (let ((knoten1 nachfolger2) (nachfolger1 (gensym))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list 'hole-objekt-unter ort) 'situation) (caddr ort)))

(defun oben-auf (objekt «optional nachfolger2) (cond ((equal nil nachfolger2) (initialisierung) (setq nachfolger2 nachfolger))) (let ((knoten1 nachfolger2) (nachfolger1 (gensym))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list 'oben-auf objekt) 'situation) (list 'oben-auf objekt)))

(defun neu-oben-auf (objekt ort &optional nachfolger2) (cond ((equal nil nachfolger2) (initialisierung) (setq nachfolger2 nachfolger))) (let ((knoten1 nachfolger2) (nachfolger1 (gensym))) (anfuegen nachfolger1 knoten1) (putprop nachfolger1 (list 'neu-oben-auf objekt ort) 'situation) (list 'neu-oben-auf objekt ort)))

(defun anfuegen (k e) (putprop k e 'knoten) (putprop e (append (get e 'nachfolger) (list k)) 'nachfolger))

(defun sag-wie (situation)

(wie1 (list nachfolger) situation))

(defun wie1 (liste situation) (cond ((null liste) nil) ((equal Situation (get (car liste) 'situation)) (mapcar #'(lambda (e) (print (get e 'situation))) (get (car liste) 'nachfolger)) T) (T (wie1 (append (get (car liste) 'nachfolger) (cdr liste)) situation))))

(defun sag-weshalb (situation) (weshalb1 (list nachfolger) situation))

(defun weshalb1 (liste situation) (cond ((null liste) nil) ((equal situation (get (car liste) 'situation)) (print (get (get (car liste) 'knoten) 'situation)) T) (T (weshalb1 (append (get (car liste) 'nachfolger) (cdr liste)) situation))))

(defun sag-warum (situation) (warum1 (list nachfolger) situation))

(defun warum1 (liste situation) (cond ((null liste) nil) ((equal situation (get (car liste) 'situation)) (print (get nachfolger 'situation)) T) (T (warum1 (append (get (car liste) 'nachfolger) (cdr liste)) situation))))

(defun ausgabe (plan) (pp (reverse plan)))

Listing 1


Links

Copyright-Bestimmungen: siehe Über diese Seite
Classic Computer Magazines
[ Join Now | Ring Hub | Random | << Prev | Next >> ]