Durch den Public-Domain-Service erhÀlt jeder Atari-ST-Besitzer die Möglichkeit, die Programmiersprache Prolog zu nutzen.
Auf der Diskette PD 11 ist die Implementation TOY PROLOG ST zu haben. Nun sind verschiedene Interpreter anderer Rechner mit einer gröĂeren Zahl vordefinierter PrĂ€dikate ausgestattet. Dadurch wird die PortabilitĂ€t von Prolog-Programmen eingeschrĂ€nkt. Ein groĂer Teil des TOY-Prolog ist aber bereits in Prolog selbst geschrieben. Wir können daher genauso einige weitere PrĂ€dikate in Prolog schreiben und dem Interpreter hinzufĂŒgen. Die Programmierung solcher PrĂ€dikate soll in diesem Artikel besprochen werden.
Programmiersprachen wie Pascal, BASIC oder Fortran bestehen aus einer Folge von Anweisungen, die der Computer erfĂŒllen solI. Da heiĂt es z.B.' Drucke "Hallo" oder 'Gehe nach 200'. Prolog hingegen baut auf dem Begriff des PrĂ€dikats auf. Ein solches kann man sich am ehesten als Frage vorstellen. Nehmen wir zur Veranschaulichung das PrĂ€dikat sum. Rufen wir es auf als ,sum(2,3,5)', stellen wir die Frage:' Ist 2+3=5?' Da das richtig ist, erhalten wir die Antwort' yes'. Ganz analog wird dagegen die Frage ,sum( 1,3,5)' mit 'no' beantwortet.
Wollen wir sum zur Berechnung einer Summe heranziehen, mĂŒssen wir eine Variable benutzen, Âsum( 1 ,3,X)'. Damit fragen wir: ,FĂŒr welches X gilt l+3=X?' Der Interpreter sucht dann eine Lösung und findet X=4. Genauso können wir fragen: ,FĂŒr welches X gilt 2+X=7?' Dazu mĂŒssen wir ,sum(2,X,7)' eingeben und bekommen die Antwort ,X=5.'
Variablen beginnen ĂŒbrigens mit einem groĂen Buchstaben, PrĂ€dikate mit einem kleinen. Zur Syntax ist weiter noch zu sagen, daĂ jede Anfrage mit einem Punkt abgeschlossen wird. Ist eine Lösung fĂŒr eine Frage gefunden worden, wird diese ausgegeben. und der Interpreter wartet, was er nun tun soll. Soll er eine weitere Lösung des Problems suchen, dann muĂ ein : eingegeben werden, ansonsten ein beliebiges anderes Zeichen, beispielsweise ein Punkt.
Sehen wir uns nun einen konkreten Anwendungsfall an. Hierzu nehmen wir an, daĂ eine Anzahl von PrĂ€dikaten zum Thema FuĂball in der Datensammlung vorhanden ist. Wir wollen nun wissen, welche deutsche FuĂballmannschaft nach 1945 viermal Pokalsieger und einmal deutscher Meister war. Dazu stellen wir die Anfrage ,pokalsieger (X,4), meister (X,1)'. Diese wird beantwortet mit ,X=eintracht_frankfurt'.
Die Vorgehensweise eines Prolog-Interpreters bei der Abarbeitung einer solchen Programmzeile ist relativ schnell erklÀrt:
ZunĂ€chst wird versucht, die erste Frage zu beantworten, also hier: Welche Mannschaft wurde viermal Pokalsieger? Da kann es durchaus mehrere Lösungen geben, z.B. werde als erste ,X=fc_köln' gefunden. (Pokalsieger 1968, 1977, 1987, 1983.) Damit wird diese Lösung an die Variable X gebunden und dann versucht, das nĂ€chste PrĂ€dikat mit dieser Lösung zu erfĂŒllen. Es wird also die Anfrage ,meister(fc_köln)' gestellt. War der FC Köln (genau) einmal deutscher Meister? Antwort: Nein, er war dreimal Meister (1962, 1964, 1978).
Die Anfrage kann also nicht erfĂŒllt werden. Somit geht der Interpreter zurĂŒck zur vorherigen Anfrage und sucht eine neue Lösung fĂŒr diese. (Dies nennt man Backtracking.) Als nĂ€chstes findet er X=eintracht_frankfurt' (Pokalsieger 1974, 1975, 1981, 1988). Damit kann er wieder nach rechts gehen und die Anfrage Âmeister(eintracht_frankfurt, 1)' Diesmal wird ein 'yes' die Antwort sein, Eintracht Frankfurt war nur 1959 deutscher Meister. Weil kein weiteres PrĂ€dikat zu erfĂŒllen ist, kann die Lösung ausgegeben werden. Soll noch eine Lösung gesucht werden, geht der wieder zum vorherigen PrĂ€dikat zurĂŒck. Dort findet er aber keine mehr, daher wird 'no geantwortet.
Obwohl sich das alles recht plausibel anhört, erfordert eigenes Programmieren in Prolog doch ein totales Umdenken und vor allem viel Ăbung. Oft hat es sich gezeigt, daĂ Beispiele sehr hilfreich beim Erlernen einer Programmiersprache sind. Wer Prolog allerdings von Grund auf lernen will, der sollte sich ein Lehrbuch zulegen, Literaturhinweise finden sich am Ende des Artikels. Denn dies soll kein EinfĂŒhrungsartikel in Prolog sein. Wer Prolog schon sicher beherrscht, kann hier neue Anregungen finden. Wer noch nicht so sicher im Umgang mit der Sprache ist, kann etwas dazulernen. Und wer schlieĂlich Prolog noch gar nicht kennt, wird immerhin sehen, was man mit diesem System alles machen kann.
Wie bereits erwĂ€hnt, beziehen wir uns hier vollkommen auf TOY PROLOG ST. Wir werden dem Interpreter einige PrĂ€dikate hinzufĂŒgen, die TOY-Prolog an andere Implementationen annĂ€hern sowie einige, die das Arbeiten mit Toy-Prolog erleichtern.
Neue ListenprÀdikate
Eine wichtige Datenstruktur in Prolog ist die Liste, die man sich am einfachsten als AufzĂ€hlung von Daten vorstellen kann. Zur Darstellung einer Liste gibt es verschiedene Notationen. Am bequemsten benutzt man die eckigen Klammern, z.B. [a,s,d]. Einige PrĂ€dikate fĂŒr Listen sind in Listing 1 angegeben.
first(L,X) ist wahr, falls X das erste Element der Liste L ist. Was wir hierbei vor uns haben, ist eine Regel. Der Operator :-ist zu interpretieren als "falls". Die gesamte Regel wĂŒrde so lauten: first(L,X) ist wahr, falls L=[X|R] wahr ist. Was bedeutet [X|R]? Durch die eckigen Klammem wird wieder eine Liste gekennzeichnet. "1" ist der Kopf-Rest-Separator, d.h. die Variable X links davon kennzeichnet das erste Element, die Variable R rechts die Restliste. WĂŒrden wir z.B. die Anfrage [a,s,d,f]=[K|R] stellen, wĂŒrde sie mit K=a, R=[s,d,f] beantwortet. Der Wert, den R durch die Anfrage erhĂ€lt, ist fĂŒr uns unwichtig. Deshalb taucht er auch nur auf der rechten Seite der Regel auf append(L,M,N) ist ein PrĂ€dikat, das bei den meisten Interpretern bereits vorhanden ist. Hier wird getestet, ob N die Liste ist, die durch AneinanderhĂ€ngen der Listen L und M entsteht. NatĂŒrlich können an jeder Stelle Variablen stehen.
append wird durch zwei Regeln rekursiv realisiert. Die erste Regel besagt, daĂ die leere Liste [] verkettet mit einer beliebigen Liste L wieder L ergibt. Bei der Lösungssuche wird zuerst versucht, diese Regel zu erfĂŒllen. Dies wird im allgemeinen nicht gelingen. da der Benutzer meistens nichtleere Listen angeben wird. Da aber durch die Rekursion irgendwann dieser Fall doch auftritt, benötigen wir diese Regel als Randbedingung. Das Ausrufezeichen heiĂt in Prolog "Cut". Es bewirkt hier, daĂ nur eine Lösung gefunden werden soll. Es wĂ€ren durchaus mehrere möglich, wenn append mit Variablen in den ersten beiden Argumenten aufgerufen wird. Aber durch den Cut wird das Auffinden weiterer Lösungen unterbunden.
Die zweite Regel lautet nun: append([K|R1],L,[K|R2]) ist wahr, falls append(R1,L,R2) wahr ist. D.h. die Anfrage wird auf den einfacheren Fall zurĂŒckgefĂŒhrt, bei dem die ersten Elemente der jeweiligen Listen abgetrennt sind, was wieder mit dem Kopf-Rest-Separator gemacht wird. Dies geht jedoch nur dann gut, wenn beide Kopf-Elemente tatsĂ€chlich identisch sind, denn sie werden durch dieselbe Variable K reprĂ€sentiert. Andernfalls schlĂ€gt die Anfrage fehl.
Beim PrĂ€dikat last(L,X), das uns das letzte Element einer Liste liefert, verwenden wir append: last(L,X) ist wahr, falls L durch AneinanderhĂ€ngen einer beliebigen Liste und [Xl entsteht. Auf Ă€hnliche Art hĂ€tten wir auch first definieren können. Aber die Verwendung von append hĂ€tte Geschwindigkeit bei der AusfĂŒhrung gekostet. Bei last hingegen kommen wir an einer Rekursion nicht vorbei, da eine Liste intern baumartig dargestellt ist und das letzte Element das am weitesten von der Wurzel stehende Element des Baums ist.
Das I-te Element X der Liste L erhalten wir durch item(I,L,X). Die Randbedingung sagt diesmal aus, daĂ X das erste Element der Liste ist, falls X der Kopf der Liste ist, so formuliert eine triviale Aussage. Der Cut sorgt wieder dafĂŒr, daĂ nur eine Lösung gesucht wird. Der Rest geschieht wie bei append rekursiv. Um die Listenelemente zu zĂ€hlen, verwenden wir hier eine Hilfsvariable J. Diese wird um 1 kleiner als I. Anstelle von = mĂŒssen wir hier den Operator is verwenden, da is arithmetische AusdrĂŒcke auswertet, = aber nicht. Die Rekursion reduziert die Anfrage jeweils auf den einfacheren Fall einer um 1 kĂŒrzeren Liste. Dies geht so lange, bis X als erstes Element einer Liste dasteht oder die Liste durch die Rekursion leer geworden ist. Listing 2 zeigt, auf welche verschiedenen Arten die neu definierten PrĂ€dikate aufgerufen werden können. Die Zeichen ?-werden vom Interpreter vorgegeben und erinnern den Benutzer daran, daĂ er nun eine Anfrage stellen kann. Das letzte Beispiel zeigt den Aufruf der neu definierten PrĂ€dikate fĂŒr Ein/Ausgabe, die nun besprochen werden sollen.
PrĂ€dikate fĂŒr Ein/Ausgabe
Die genauen Dehnitionen sind in Listing 3 angegeben. In Anlehnung an Pascal definieren wir zunĂ€chst das PrĂ€dikat writeln(X), welches nach Ausgabe von X einen Zeilenvorschub durchfĂŒhrt. In Toy-Prolog wird ein Zeilenvorschub durch nl gekennzeichnet. Das PrĂ€dikat tab(X) gibt X Leerzeichen aus. Die Definition erfolgt wieder rekursiv. Als Randbedingung geben wir tab(O) an. Die dritte, rekursive Regel gibt jeweils ein Leerzeichen mittels weh aus und bezieht sich dann auf den nĂ€chst niedrigeren Fall. Dies geht so lan-ge, bis tab(O) aufgerufen wird. Bei Aufruf mit einem negativen Wen wĂŒrde die Rekursion so bis ins Unendliche gehen. Deshalb mĂŒssen wir da einen Riegel vor-schieben und diesen Fall ausschlieĂen.
In der zweiten Regel wird zunÀchst gete-stet, ob X
Listing von PrÀdikaten
In Prolog ist es möglich, mit dem PrĂ€dikat Listing den Text sĂ€mtlicher PrĂ€dikate aufzulisten. Ein unglĂŒcklicher Umstand bei Toy-Prolog ist es, daĂ hierdurch auch die SystemprĂ€dikate betroffen sind, die ja auch in Prolog geschrieben sind. Anstelle eines kurzen Listings der wenigen selbstdefinierten PrĂ€dikate, an denen man gerade arbeitet, erhĂ€lt man so fast den gesamten Inhalt der Systemdatei ausgegeben. Was wir nun selbst programmieren wollen, ist ein PrĂ€dikat, das nur die von uns definierten PrĂ€dikate listet.
Dazu mĂŒssen wir einen kleinen Eingriff in das System vornehmen. Wir nehmen einmal an, wir lesen alle unsere PrĂ€dikate jeweils mit consult ein. Dann definieren wir einfach ein neues PrĂ€dikat bad, welches genau dasselbe wie consult erledigt, aber zusĂ€tzlich jedes eingelesene PrĂ€dikat markiert. Das PrĂ€dikat list wird dann nur diejenigen PrĂ€dikate auflisten, die mittels bad markiert wurden.
GlĂŒcklicherweise mĂŒssen wir uns die Programmierung von bad nicht von Grund auf neu ĂŒberlegen, denn mit Listing können wir ja den Text von consult erhalten. In Listing 4 sehen wir das Ergebnis. Die PrĂ€dikate see, seeing und seen öffnen und schlieĂen Dateien, echo und noecho sorgen fĂŒr die Wiedergabe auf dem Bildschirm. Von Interesse ist fĂŒr uns nur get-prog. Er stellt die Schleife dar, in der die PrĂ€dikate eingelesen und definiert werden.
Wie wir das nun verwenden mĂŒssen, können wir in Listing 5 sehen. bad schreiben wir wie consult, ersetzen aber read-prag durch readprogram. Dieses wird wie readprog definiert, jedoch getprog wird nun getprogram genannt. getprogram schlieĂlich wird wie getprog definiert, nur schieben wir make_listable(X) ein. Dies sorgt dafĂŒr, daĂ das PrĂ€dikat P, das durch die Regel X definiert ist, markiert wird. Die Markierung geschieht durch HinzufĂŒgen der Klausel listable(P) zur Datenbank. (Eine Klausel ist eine Regel ohne Bedingung.) Weil jede Datei durch end abgeschlossen werden muĂ, wird somit auch jedesmal ein listable(end) der Datenbank hinzugefĂŒgt. Dies ist aber nicht erwĂŒnscht, durch retract (listable(end)) wird es wieder entfernt.
make_listable(X) holt sich den Namen P des PrĂ€dikats, das durch X definiert wird, und prĂŒft, ob listable(P) bereits vorhanden ist. Wenn nein, wird es hinzugefĂŒgt.
Auf das PrĂ€dikat pred(X,K) soll nicht so ausfĂŒhrlich eingegangen werden. Es prĂŒft, ob K der Name des PrĂ€dikats ist, das durch die Regel X definiert wird. Eine Fallunterscheidung muĂ vorgenommen werden. Denn einmal kann X eine Regel mit dem Operator :- sein, zum anderen eine grammatische Regel mit dem Operator->. AuĂerdem ist es möglich, daĂ X eine Klausel ist. Das PrĂ€dikat list holt sich mit listable(X) jeweils ein markiertes PrĂ€dikat und gibt mittels listing(X) dessen Programmtext aus. Das fail am Ende sorgt dafĂŒr, daĂ die Anfrage fehlschlĂ€gt und durch Backtracking nacheinander alle Lösungen von listable(X) gefunden werden.
Fassen wir also zusammen: Laden wir eine Datei mit load, werden alle PrĂ€dikate daraus markiert. list gibt eine Auflistung nur der markierten PrĂ€dikate. Wollen wir Eingaben im Direktmodus machen, so können wir eine Markierung auch erreichen. Wir mĂŒssen nur load(user) eingeben. AuĂerdem kann ein PrĂ€dikat nachtrĂ€glich markiert werden. Nötig ist ein HinzufĂŒgen von listable(P) zur Datenbank.
Aus GrĂŒnden der Einheitlichkeit definieren wir zusĂ€tzlich list(P), welches nur den Text des PrĂ€dikats P listet. listable gibt eine Liste aller markierten PrĂ€dikate. Da die Lösungen von listable(X), das als erstes aufgerufen wird, explizit in der Datenbank stehen, werden sie vom Interpreter leicht gefunden.
oblist gibt die Namen aller in Prolog definierten PrÀdikate aus, also auch die aus der Systemdatei. Erzeugt werden sie als Lösungen der Anfrage proc(X), die restlichen PrÀdikate aus dieser Regel dienen nur der optischen Autbereitung, so etwa Abtrennen der Argumente etc. oblist arbeitet ebenso wie listable mit der Schleifenbildung durch fail.
Impiementierung des Break Die Anweisung break unterbricht den Programmlauf und fĂŒhrt den Benutzer wieder auf die oberste Interpreterebene. Zur Kennzeichnung wird eckigen Klammern das Break-Level angezeigt, denn Verschachtelungen sind möglich. Nun können beliebige Anfragen gestellt werden. Durch Eingabe von cont gelangt man wieder ein Level tiefer. Das dort begonnene Programm wird nun fortgefĂŒhrt.
Um dies nachtrĂ€glich in Prolog zu programmieren, mĂŒssen wir uns dieses Mal die Interpreterschleife ansehen. Der Dokumentation zu Toy-Prolog entnehmen wir, daĂ der sogenannte Ă€uĂere Interpreter durch das PrĂ€dikat ear gestartet wird. Mit listing verschaffen wir uns wieder den Programmtext (siehe Listing 6).
Ear ruft loop auf. In der Schleife loop wird die Benutzereingabe gelesen und ausgefĂŒhrt. Ein paar Worte mĂŒssen zu dem Aufruf von loop gemacht werden. loop ist eine Endlosschleife. DafĂŒr sorgt die repeat/fail-Konstruktion. Riefe man dieses PrĂ€dikat einfach durch Angabe des Namens auf, wĂŒrde es nie zum Ende kommen. Das ist aber nicht erwĂŒnscht. Durch den Aufruf tag(loop) erhalten wir die Möglichkeit, spĂ€ter von auĂen auf die ErfĂŒllung des PrĂ€dikats loop EinfluĂ zu nehmen. Denn ein Aufruf von tagfail(loop) bewirkt, daĂ loop sofort fehlschlĂ€gt, was damit auch fĂŒr ear gilt. Der Interpreter versucht dann, ear mittels der zweiten Regel zu erfĂŒllen. Dort wird durch halt der Interpreter abgebrochen.
Die Idee zur Programmierung des Break ist nun die folgende: Das PrÀdikat break wird analog zu ear programmiert. Die ausgegebenen Meldungen werden jedoch anders aussehen. ear ruft statt loop das PrÀdikat breakjoop auf. Dieses sieht fast genauso wie loop aus, nur wird das Break- Level in eckigen Klammem mit ausgegeben. cont entspricht stop.
Die Realisierung finden wir in Listing 7. Zur Kennzeichnung des Break-Levels existiert jeweils ein PrĂ€dikat breakJevel(X). Durch Aufruf von die-sem wird X auf den entsprechenden Wert gesetzt. Geht man in ein anderes Break-Level, muĂ dieses PrĂ€dikat gelöscht und durch das mit dem neuen Wen ersetzt werden. So ist zu Beginn das PrĂ€dikat break_level(0) vorhanden. Bei Aufruf von break wird es gelöscht und durch break_level(1) ersetzt. Eingabe von cont macht dies wieder rĂŒckgĂ€ngig.
Listing 8 zeigt uns die Wirkungsweise von break. Nachdem das Atom 'eins' ausgegeben wurde, gelangen wir auf Break-Level 1. Dort können wir uns 'zwei' ausgeben lassen. Nach Eingabe von cont gelangen wir zurĂŒck. Dort muĂ die angefangene Programmzeile noch beendet werden, d.h. 'drei' wird ausgegeben.
Einen echten Anwendungsfall sehen wir in Listing 9. Angenommen, wir haben das PrĂ€dikat tier fĂŒr eine Anzahl von Daten in der Datenbank. tier(elefant) beispielsweise soll heiĂen: Der Elefant ist ein Tier. Die Aufgabe soll darin bestehen, fĂŒr jedes Tier, das vorhanden ist, ein PrĂ€dikat ordnung einzugeben, das die biologische Ordnung des jeweiligen Tieres angibt. Dazu können wir den Break benutzen.
Zur Illustration geben wir zunĂ€chst mit assert drei Daten ein. Bei der folgenden Anfrage findet tier(X) jeweils eins dieser Tiere, write(X) gibt es aus. Der dann folgende Break bringt uns auf die Interpreterebene, in der wir Eingaben machen können. Wir fĂŒgen dann das entsprechende PrĂ€dikat ein. Zuerst wird fledermaus gefunden. Wir wissen, die Fledermaus gehört zur Ordnung der Flattertiere und geben dies ein. Nach cont wird die Anfrage fortgesetzt, wir erhalten das nĂ€chste Tier. Das fail bildet wieder eine Schleife.
Einbau der neuen PrÀdikate
Das Toy-Prolog-System erlaubt es nun, die neu definierten PrĂ€dikate der Systemdatei hinzuzufĂŒgen, so daĂ sie nach Starten des Systems direkt zur stehen. Wie das gemacht wird, jetzt noch besprechen.
ZunĂ€chst schreiben wir mit einem Editor alle PrĂ€dikate, die wir in unserem System neu haben wollen, in eine einzige Datei. Wir nennen sie jetzt einmal ERWEIT. Die letzte Zeile bildet die Anweisung end., der Punkt darf nicht fehlen. Jetzt starten wir den Prolog-Interpreter und rufen das PrĂ€dikat translate auf. Hiermit ĂŒbersetzen wir den von uns geschriebenen Code in die Syntax des inneren Interpreters. Der Aufruf lautet translate(erweit, erw). Der gebildete Code wird in der Datei ERW abgelegt.
Mit stop beenden wir Prolog und starten wieder den Editor. Hier laden wir zunĂ€chst die Datei ERW und löschen deren letzte drei Zeilen. Diese bestehen aus einem Doppelpunkt, dem Wort seen und der Zeichenkombination []#. Nun laden wir die Datei SYSFILE.TOY und gehen mit dem Cursor an das Ende des Textes. Dort finden wir nach einem Doppelpunkt einige Anweisungen, die direkt ausgefĂŒhrt werden sowie die Definition des letzten PrĂ€dikats, unprotect. Dieses endet mit []. Zwischen [] und den Doppelpunkt wird die geĂ€nderte Datei ERW nun geladen.
Das war fast schon alles. Wie wir aber sehen, werden durch protect alle geladenen PrĂ€dikate geschĂŒtzt. Das PrĂ€dikat break_level wird jedoch bei Aufruf von break verĂ€ndert, was bei einem geschĂŒtzten PrĂ€dikat nicht möglich ist. Deshalb mĂŒssen wir nur hierfĂŒr den Schutz rĂŒckgĂ€ngig machen. Wie die letzten Zeilen von SYSFILE.TOY aussehen sollten, zeigt Listing 10.
1: tab(0)..
2: tab(X) :- x<0, !, fail.
3: tab(X) :- Y is X - 1, wch(' '), tab(Y).
4:
5: writeln(X) :- write(X), nl.