Prolog. Kapitel Syntax und Semantik von Prolog

Kapitel 4 Prolog In diesem Kapitel wollen wir uns mit Logik-Programmierung besch¨aftigen, konkret mit der Programmiersprache Prolog. Bei kommando-ori...
4 downloads 2 Views 150KB Size
Kapitel 4

Prolog In diesem Kapitel wollen wir uns mit Logik-Programmierung besch¨aftigen, konkret mit der Programmiersprache Prolog. Bei kommando-orientierten Programmiersprachen wie Java und C, aber auch Assembler, Fortran, Pascal usw. ist ein Programm eine Abfolge von Statements, Tests, Schleifen, Unterfunktionen usw., die von einem Interpreter oder (nach einer Kompilierung) von einem Prozessor abgearbeitet werden. Prolog geht einen anderen Weg. Ein Prolog-Programm kann als eine Menge von Axiomen, den Regeln und Fakten, aufgefasst werden. Ein Programm-Aufruf ist die Beantwortung einer Anfrage, d.h. das Testen, ob eine Aussage aus den Regeln und Fakten hergeleitet werden kann. Dieser Test wird automatisch im Hintergrund von einer sogenannten Inferenzmaschine (Inferenz Folgerung) ausgef¨uhrt. Wir wollen diese Grundbegriffe (Fakten, Regeln, Anfragen) kurz einf¨uhren, um uns einen ersten Eindruck von (und Lust auf) Prolog zu verschaffen. In den darauf folgenden Abschnitten werden wir die Funktionsweise von Prolog formal erl¨autern und zeigen, was Prolog mit der Aussagenlogik und der Pr¨adikatenlogik zu tun hat. Wir werden die Bedeutung der Konzepte Resolution, Hornformeln, Substitution und Skolemform f¨ur die Logik-Programmierung sehen und dann die komplexeren Sprachkonstrukte kennenlernen. Den Abschluss bildet ein Beispielprogramm aus der Praxis, das die M¨achtigkeit und Eleganz der Logik-Programmierung demonstriert.

4.1

Syntax und Semantik von Prolog

In diesem Abschnitt wollen wir die Syntax von Prolog–Programmen sowie deren Semantik, d.h. ihre pr¨adikatenlogische Entsprechung behandeln. Wir werden neben den Prolog–Beispielen auch die a¨ quivalente pr¨adikatenlogische Formel angeben. Eine Zusammenfassung der Syntax findet sich in Tabelle 4.1. Variablen, Atome, Terme, Strukturen. Die Grundkonstrukte von Prolog sind Variablen und Atome. Variablen sind alle gross geschriebenen Worte, wie z.B. X, N15 und Ypsilon. Atome sind alle kleingeschriebenen Worte, wie z.B. vater, hase, a und bRUDER27. Die Atome sind die Platzhalter f¨ur die Konstanten, die Funktionensymbole und die Pr¨adikatensymbole in der Pr¨adikatenlogik. Man muss aber unbedingt den syntaktischen Unterschied von Prolog zur Pr¨adikatenlogik beachten, um nicht durcheinanderzukommen: in der Pr¨adikatenlogik wurden die Variablen kleingeschrieben, die Pr¨adikatensymbole gross und die Funktionensymbole wieder klein. Es ist in Prolog nicht aus einem Namen erkenntlich, ob es sich um eine Funktion oder ein Pr¨adikat handelt. Das wird aber immer sofort aus der Verwendung im Programm sichtbar. Auf der n¨achsten Stufe stehen die Strukturen und Terme. Sie sind wie folgt rekursiv definiert: Ein Term ist eine Variable oder eine Struktur; eine Struktur ist entweder ein Atom oder ein Atom gefolgt von

91

Was? Atom

Definition kleingeschriebenes Wort

Beispiel a, bruder, zwEI2

Variable

grossgeschriebenes Wort

X, Bruder, Y15

Sonderzeichen

:- (das wenn“) ” ?- (das Anfragensymbol) ,“ (das und“ in Regeln und Anfragen) ” ” .“ (das Klauselendzeichen) ” (“ (die Klammern zum Erschaffen von ” )“ Strukturen) ” Variable Struktur

Term Struktur

Atom Atom ( Term ) Atom ( Term , Term ) Atom ( Term , Term ,



X15 adam f(x) vater(adam,peter) add(f(f(a)),f(a),X)

)

Fakt

Struktur .

liebt(gott,X).

Regel

Struktur :- Struktur . Struktur :- Struktur , Struktur . Struktur :- Struktur , Struktur ,

kauft(sibylle,X):-billig(X). opa(X,Y):-vater(X,Z),vater(Z,Y).

Programm

(leeres Programm) Regel Programm Fakt Programm

Anfrage

?- Struktur . ?- Struktur , Struktur . ?- Struktur , Struktur ,



.

?-opa(andrea,claus). ?-add(a,f(a),X),add(X,Y,f(f(a))). 

.

Tabelle 4.1: Syntax von Prolog einer Liste von Termen, die in Klammern eingeschlossen und durch Kommas getrennt sind. Beispiele f¨ur Terme sind PlatzDa, wegda(Du), komme(ich,jetzt), wobei der erste keine Struktur ist. Auf oberster Ebene stehen in Prolog die Klauseln. Es gibt drei Arten von Klauseln: Fakten, Regeln und Anfragen. Dies ist eine starke Parallele zu den (aussagenlogischen) Hornformeln in Abschnitt 1.21. Fakten und Regeln. Ein Fakt ist eine Struktur, die von einem Punkt gefolgt wird. In der Pr¨adikatenlogik entspr¨ache das einem Pr¨adikat, welches f¨ur sich steht. Wenn eine Variable vorkommt, so muss man sie  sich durch ein quantisiert vorstellen: habeHunger. vater(frank,ruedi). add(a,a,a). add(a,X,X)

HabeHunger  Vater  f rank  ruedi  Add  a  a  a   x Add  a  x  x 

Semantisch lassen sich die Fakten wie Pr¨adikate mit einer gewissen Anzahl von Argumenten interpretieren. Der zweite Fakt liest sich dann umgangssprachlich: Frank ist der Vater von Ruedi“, der vierte Fakt: ” F¨ur alle x gilt 0  x x“ (die Konstante a wird wie in der Pr¨adikatenlogik als Null interpretiert). ” Die zweite Klauselart ist die Regel. Sie besteht aus einer Struktur (dem Kopf“), der Zeichenkette :-“, ” ” einer Liste von Strukturen (dem Rumpf“), ebenfalls abgeschlossen durch einem Punkt. Das :-“ wird ” ” 92

als wenn“ ausgesprochen und wird mit dem umgekehrten Folgerungspfeil u¨ bersetzt. Die Strukturen ” im Rumpf werden durch Kommas getrennt, die als und“ ( ) interpretiert werden: ” endloseFreiheit(X):-¨ uberdenWolken(X).  ¨ x  EndloseFreiheit  x  UberdenWolken  x  add(f(X),Y,f(Z)):-add(X,Y,Z).    x y z  Add  f  x   y f  z  Add  x  y z  opa(X,Y):-vater(X,Z),vater(Z,Y). 





x y z  Opa  x  y   Vater  x  z  Vater  z  y 

Wir wollen versuchen, die Regeln umgangssprachlich zu verstehen. Wir behaupten, die Regel opa(X,Y):-vater(X,Z),vater(Z,Y).

u¨ bersetzt sich folgendermassen ins Deutsche: Herr X ist der Grossvater von Herrn Y, wenn es einen ” Herrn Z gibt, dessen Vater X ist und der selber der Vater von Y ist.“ Bisher hatten wir gesagt, dass alle ¨ Variablen allquantisiert sind. Dass die o.g. Ubersetzung trotzdem richtig ist, sehen wir an folgender pr¨adikatenlogischer Umformung:   

 

 







x  x  x  x  x



y z  Opa  x  y   Vater  x  z 

 y z  Opa  x  y  Vater  x  z 

 y  Opa  x  y  z  Vater  x  z 

y  Opa  x  y  z  Vater  x  z 

y  Opa  x  y   z  Vater  x  z 

Vater  z  y  Vater  z  y  Vater  z  y  Vater  z  y  Vater  z  y 

(Def. von und DeMorgan) (z kommt in Opa  nicht frei vor)  (    ) Heureka!

Fakten und Regeln zusammengenommen stellen das Prolog–Programm dar. Als pr¨adikatenlogische Formeln sind die Klauseln durch verkn¨upft. Anfragen. Man betrachtet ein Prolog–Programm gerne als Datenbank von Wahrheiten, an die man Anfragen (auch gerne Zielklauseln genannt) stellen kann. Eine Anfrage beginnt mit einem ?-“, dem An” fragesymbol (als Stimmt es, dass  ?“ zu lesen). Es wird von einer Liste von Strukturen gefolgt, die ” wieder durch Kommas getrennt und durch einen Punkt abgeschlossen wird. Auch diese Liste nennt man Rumpf. In eine pr¨adikatenlogische Form bringt man die Anfragen (der Hornklausel–Enthusiast ahnt es schon) durch Davorschreiben eines Negationszeichens und der Interpretation aller Kommas als und“: ” ?-froh(reinhard). ?-opa(claus,X). ?-add(X,Y,f(f(f(a)))),add(X,f(a),Y).

  

Froh  reinhard  x  Opa  claus  x   x y  Add  x  y f  f  f  a  Add  x  f  a   y 

Dies ist ganz wie bei den Hornformeln: um eine Anfrage aus einer Klauselmenge zu folgern, gen¨ugte es, ihr Gegenteil anzunehmen und die unerf¨ullbare Formel zu folgern. Auch wenn Variablen ins Spiel kommen, stimmt dieses Bild: Im letzten Beispiel ist die pr¨adikatenlogische Formel a¨ quivalent zu  xOpa  claus  x  , was der Frage entspricht Stimmt es, dass es ein X gibt, dessen Grossvater Claus ist?“ Beziehungsweise ” gar ein zweifelndes Hat Claus Enkel?“ Die letzte Beispielformel w¨urde sich als Gibt es x und y, deren ” ” Summe 3 und deren Differenz 1 ist?“ u¨ bersetzen.

4.2 4.2.1

Prolog und Hornklauseln Einfuhrende ¨ Beispiele

Betrachten wir das Beispielprogramm fiktiver Familienbeziehungen: 93

1: 2: 3: 4: 5: 6:

vater(adam,bertold). vater(bertold,claus). vater(adam,dieter). vater(ede,frank). opa(X,Y):-vater(X,Z),vater(Z,Y). onkel(X,Y):-vater(Z,X),opa(Z,Y).

Die vier Fakten lassen sich in einem Stammbaum verdeutlichen: Adam Ede Bertold

Dieter Frank

Claus

Die anderen zwei Zeilen enthalten die Regeln. Alle Variablen sind allquantisiert. Daher k¨onnen wir f¨ur X, Y, Z beliebige Terme einsetzen (substituieren) und erhalten eine Folgerung der Regel. Zum Beispiel ergibt die Substitution [X/adam][Y/claus][Z/bertold] die Regel 5 : opa(adam,claus):-vater(adam,bertold),vater(bertold,claus). Eine Prolog–Klausel (ein Fakt oder eine Regel), die keine Variable enth¨alt, k¨onnen wir als (aussagenlogische) Hornklausel auffassen, indem wir jeden Term mit einer entsprechenden aussagenlogischen atomaren Formel identifizieren. vater(adam,bertold). vater(bertold,claus). opa(adam,claus):vater(adam,bertold), vater(bertold,claus). ?-opa(adam,claus).

Va  b Vb  c

#

"!

Oa  c  Va  b  Vb  c 

Oa  c

In Abschnitt 1.21 haben wir gesehen, dass es einen Algorithmus gibt, um zu entscheiden, ob aus Hornformel F ein weiterer Fakt A folgt. Dazu nahmen wir den negierten Fakt  A als Anfrage dazu und versuchten mit Hilfe von Resolutionen die leere Klausel $ zu folgern. Wenn uns das gelang, so wussten wir, dass die gesamte Formel F % A unerf¨ullbar sein musste und darum A aus F folgen muss. Hierzu gab es zwei Strategien, eine bottom–up–Strategie und eine top–down–Strategie. Die top–down–Strategie geht von der Anfrage aus, sucht nach Regeln oder Fakten, aus denen die Anfrage folgen k¨onnte und nimmt ggf. die Hypothesen dieser Regel als neue Anfragen. Dieser Algorithmus arbeitet rekursiv (f¨ur jede Anfrage werden alle m¨oglichen Regeln und Fakten gesucht und nacheinander ausprobiert), bis entweder die leere Anfrage gefunden wurde (A folgt aus F) oder alle M¨oglichkeiten ohne Erfolg ausprobiert wurden (A folgt nicht aus F). Prolog folgt dieser top–down–Strategie. Der Abschnitt 4.3 wird sich ausf¨uhrlich mit den Details besch¨afti¨ gen. Wir wollen jetzt schon am Beispiel der Anfrage opa(adam,claus) einen Uberblick geben. Links ist der Baum der Hornklauselresolutionen, rechts die entsprechenden Resolutionen der Prolog–Klauseln.

94

Diese B¨aume werden auch Resolutionsb¨aume oder Ableitungsb¨aume genannt: 

Oa  c

Oa  c  Va  b  Vb  c

?-opa(adam,claus).

opa(X,Y):-vater(X,Z),vater(Z,Y). [X/adam][Y/claus]

PSfrag replacements



Va  b Vb  c 

Va  b

?-vater(adam,Z),vater(Z,claus).

vater(adam,bertold).

[Z/bertold]



Vb  c

$

Vb  c

?-vater(bertold,claus).

vater(bertold,claus).

?-.

An diesem Beispiel sieht man auch, dass Prolog nicht alle Variablen sofort aus einer Regel heraussubstitutiert, sondern nur immer so viele, wie gerade notwendig sind. In der ersten Zeile sind dies nur X und Y, Z wird noch in die zweite Zeile u¨ bernommen. F¨ur die Resolution ist es n¨amlich lediglich notwendig, dass (nach Variablensubstitutionen) opa(adam,claus) und opa(X,Y) gleich sind. Im allgemeinen muss zur Resolution der Kopf der Regel und die (erste Struktur der) Anfrage u¨ bereinstimmen. Das Auffinden ¨ einer Substitution, die diese Ubereinstimmung bewirkt, wird als Unifikation bezeichnet (siehe Abschnitt 4.3.1). Die Anfrage ?-opa(adam,claus) u¨ bersetzte sich in die Frage Ist Adam der Grossvater von Claus?“ ” Die Antwort ist mit der Anwendung der o.g. umgangssprachlichen Regel Ja“, denn Adam ist der Vater ” von Bertold, der selbst wieder der Vater von Claus ist. Wir wollen versuchen, die Ableitung in umgangssprachlichen S¨atzen nachzuvollziehen: Um die Regel F¨ur alle Menschen X und Y  “ zu verwenden, ” haben wir f¨ur X Adam und f¨ur Y Claus eingesetzt (oder substituiert). Nach der Resolution bleibt die Anfrage ?-vater(adam,Z),vater(Z,claus) bzw. die Frage: Existiert ein Mensch Z, dessen Vater Adam ” ist und der selbst Vater von Claus ist?“ Das Durchsuchen der Fakten liefert Adam ist Vater von Bertold“. ” Wir setzen in unserer Regel Bertold in Z ein und m¨ussen nun u¨ berpr¨ufen, ob Bertold Vater von Claus ist. Dies ist wieder ein Fakt und wir k¨onnen Ja“ antworten. ” Wann antwortet Prolog mit Nein“? Hier muss man Acht geben, denn dies geschieht nicht nur, wenn die ” Anfrage falsch ist, sondern immer, wenn Prolog die Anfrage nicht aus den Fakten und Regeln folgern kann. Zum Beispiel kann die Anfrage ?-onkel(dieter,frank) nicht auf die beschriebene Weise aus dem Programm gefolgert werden. Man muss sich aber klar machen, dass dies nicht heisst, dass Prolog einen Gegenbeweis gefunden hat, d.h. das Gegenteil der Anfrage aus den Regeln folgern konnte. Nein, es heisst lediglich, dass alle Versuche, die Anfrage zu beweisen, gescheitert sind. Bisher haben wir nur Ja/Nein–Anfragen gestellt ( Ist es wahr, dass Adam der Vater von Bertold ist?“). ” Es sind aber auch Fragen nach einem Wert denkbar: Was ist 2  2?“ F¨ur ein Pr¨adikat add(X,Y,Z), das ” genau dann wahr wird, wenn x  y z ist, k¨onnten wir fragen ?-add(f(f(a)),f(f(a)),X). Um solch ein Pr¨adikat zu definieren, betrachten wir das Programm: add(a,X,X). add(f(X),Y,f(Z)):-add(X,Y,Z).

Dieses Programm erinnert sehr an die rekursive Modellierung der nat¨urlichen Zahlen in der Pr¨adikaten logik. In der ersten Zeile steht der Fakt: x 0  x x“ (a steht f¨ur die Null). Die zweite Zeile enth¨alt die    ” Regel: x y z  f  x & y f  z ' ( x  y z  “. Wie auch schon bei unseren Betrachtungen zur Pr¨adika” tenlogik interpretieren wir f als Nachfolgerfunktion f  x  x  1. Die Regel entspricht also der Aussage 95

u¨ ber nat¨urliche Zahlen  x  1 ) y  x  y ) 1. Konkrete nat¨urliche Zahlen sind dann durch wiederholte Anwendung der Funktion f auf a erh¨altlich: f(f(f(a))) entspricht gerade  0  1  1  1 3. Wir wollen uns anschauen, wie man Prolog mit diesem Programm und der Anfrage ?-add(f(f(a)),f(f(a)),X) zum Rechnen bringt. Unter Anwendung nur der beiden o.g. Programmzeilen ist diese Anfrage beantwortbar. Der Ableitungsbaum sieht dann folgendermassen aus: ?-add(f(f(a)),f(f(a)),X). [X/f(Z1)]

add(f(X1),Y1,f(Z1)):-add(X1,Y1,Z1). [X1/f(a)][Y1/f(f(a))]

?-add(f(a),f(f(a)),Z1). [Z1/f(Z2)]

add(f(X2),Y2,f(Z2)):-add(X2,Y2,Z2). [X2/a][Y2/f(f(a))]

?-add(a,f(f(a)),Z2). [Z2/f(f(a))

add(a,X3,X3). [X3/f(f(a))]

PSfrag replacements $

Gleich in der ersten Zeile f¨allt auf, dass zwar sinngem¨ass die zweite Zeile des Programms verwendet wurde, die Variablen aber umbenannt wurden. Wir wollen verstehen, warum dies m¨oglich ist. Man mache sich zun¨achst klar, dass es nicht von Bedeutung ist, ob die Allquantoren der Variablen ganz aussen  ¨ x  F G  oder nur ausserhalb jeder Klausel stehen. Dies ist eine Konsequenz aus der Aquivalenz   adikatenlogische Formel u¨ bersetzt:  x F  * x G  . Das obige Programm lautet zum Beispiel in eine pr¨ 

  





x y z  Add  a  x  x  + Add  f  x   y f  z , Add  x  y z     x  Add  a  x  x  y z  Add  f  x   y f  z , Add  x  y z       x Add  a  x  x  * x y z  Add  f  x   y f  z , Add  x  y z      Add  x2  y2  z2   x1 Add  a  x1  x1  * x2 y2 z2  Add  f  x2   y2  f  z2  





¨ Die erste Aquivalenz ist Ausklammern von x, die zweite kommt von der Regel x  F G    x F -

 ¨ die Variablen einer  x G  . Wir sehen auch, dass durch die gebundene Umbenennung (dritte Aquivalenz) Klausel (eines Fakts oder einer Regel) umbenannt werden k¨onnen, ohne den Sinn der Formel (des Programms) zu ver¨andern. Das heisst, das Programm kann auch ohne Sinn¨anderung wie folgt geschrieben werden. add(a,X1,X1). add(f(X2),Y2,f(Z2)):-add(X2,Y2,Z2).

Diese Umbenennung wird von Prolog immer automatisch vorgenommen. Nur so k¨onnen wir die Regeln und Fakten viele Male unabh¨angig verwenden. Noch einmal: Prolog benennt die Variablen der verwendeten Regel (oder des verwendeten Faktes) immer so um, dass diese keine Variable mit der aktuellen Anfrage gemein hat. ¨ Wenn man den Ubergang von der ersten (urspr¨unglichen) Anfrage zur n¨achsten genau ansieht, so bemerkt man, dass mit Hilfe der Regel die Anfrage vereinfacht wurde: von Was ist 2  2?“ geht es zu Was ist 1  ” ” 2?“ u¨ ber. Dies ist ja genau der Sinn von rekursiven Programmen: In jedem Schritt wird die Aufgabe ein wenig vereinfacht, gleicht aber grunds¨atzlich der urspr¨unglichen Aufgabe. Am Ende bleibt eine einfache Anfrage ( Was ist 0  1“) u¨ brig, die nur noch einen einfachen Fakt zur Beantwortung ben¨otigt. Dies ist ” das Grundprinzip der Rekursion. Es ist extrem wichtig f¨ur das Programmieren in Prolog, da praktisch alle etwas komplexeren Programme Rekursion (meist sogar mehrfach) enthalten. 96

Die Antwort von Prolog auf diese Anfrage enth¨alt auch den errechneten Wert: Prolog antwortet X=f(f(f(f(a)))) Yes

Den Wert von X bekommt man durch Anwendung aller Substitutionen auf X: X [X/f(Z1)][X1/f(a)][Y1/f(f(a))][Z1/f(Z2)][X2/a][Y2/f(f(a))][Z2/f(f(a))][X3/f(f(a))] X [X/f(Z1)][Z1/f(Z2)][Z2/f(f(a))] f(f(f(f(a)))) 

welches dem richtigen Ergebnis 4“ entspricht. ”

4.2.2

Prologklauseln sind Hornklauseln

Um noch besser zu verstehen, was Prolog mit der Aussagenlogik und mit der Pr¨adikatenlogik zu tun hat, wollen wir Fakten und Regeln sowie Anfragen noch mehr formalisieren. Die allgemeinste Form der Regel ist B:-C1 ,  ,Cm . wobei m eine nat¨urliche Zahl ist. Wenn wir die Syntax von Prolog leicht ab¨andern, fallen auch Fakten in diese Form: F¨ur m 0 ist der Rumpf der Regel leer. Dies macht auch Sinn, denn die pr¨adikatenlogische ¨ Ubersetzung einer Regel mit den Variablen x1  xr w¨urde dann lauten: 



x1  xr  B  C1 . Cm  

F¨ur m 0 fallen dann einfach keine C1  Cm an, es ist ein Fakt. Wir f¨uhren diese Anpassung der Syntax jedoch nur in unseren K¨opfen durch, Prolog w¨urde hungrig(ich):-. nicht verstehen. Ebenso k¨onnen wir auch die Syntax der Anfragen durch ?-D1 ,  ,Dn .

formalisieren, wieder sind D1  Dn Strukturen und n eine nat¨urliche Zahl. F¨ur n 0 erhalten wir die (in ¨ Prolog legale) leere Anfrage. Die Ubersetzung in eine pr¨adikatenlogische Formel lautet dementsprechend 



x1  xs  D1 /0 Dn  

wobei wieder x1 1 xs die in der Anfrage vorkommenden Variablen sind. Wir wollen uns u¨ berlegen, welche pr¨adikatenlogischen Formeln wir als Prolog–Programme schreiben k¨onnen. Zun¨achst haben wir gesehen, dass in einer von Prolog stammenden Formel alle Quantoren vorne stehen und dass sie alle Allquantoren sind. Ausserdem ist eine solche Formel nach Wegnahme der Quantoren eine Konjunktion (Ver-und-ung) der Klauseln, der Fakten, Regeln und Anfragen. Diese Klauseln sind wiederum Disjunktionen (Ver-oder-ungen) von Strukturen, von denen alle bis auf h¨ochstens eine negiert sind. (Die Disjunktionen ohne positive Strukturen sind die Anfragen. Die Disjunktionen mit nur einer einzigen Struktur, die auch noch positiv ist, sind die Fakten. Alle anderen sind die Regeln.) Satz 4.2.1 Die pr¨adikatenlogischen Formeln, die Prolog–Programmen (inkl. Anfragen) entsprechen, sind genau die Formeln in Skolemform, deren Boole’sche Form eine Hornformel ist.

97

Ist dies eine Einschr¨ankung? Die Antwort lautet leider ja. Beispielsweise ist die Definition des onkel– Pr¨adikats durch onkel(X,Y):-vater(Z,X),opa(Z,Y). nur unvollst¨andig. Sie schliesst n¨amlich nicht aus, dass X der Vater von Y ist: auch dann gibt es ein Z, das Vater von X und Grossvater von Y ist. Eine richtige pr¨adikatenlogische Formulierung m¨usste dann lauten: 



x y Onkel  x  y  2 zVater  z  x  Opa  z  y  % Vater  x  y      x y z  Onkel  x  y  Vater  x  y % Vater  z  x  Opa  z  y 1 

Diese Formel ist zwar in Skolemform (man kann ja jede Formel in Skolemform bringen), aber ihre Boole’sche Form ist keine Hornformel, da zwei Literale positiv sind. Auch durch weitere Umformungen ist sie nicht in eine solche Form zu bringen. Wir werden jedoch am Ende dieses Kapitels einen Weg sehen, wie man mit Hilfe der expliziten Ausnutzung der Auswertungsstrategie ein Programmverhalten erreichen kann, welches die obige Formel simuliert.

98

4.3

Unifikation und Auswertungsstrategie

F¨ur Hornformeln hatten wir zwei Arten von Algorithmen, um zu entscheiden, ob sie unerf¨ullbar sind (Abschnitt 1.21): eine bottom–up–Strategie und eine top–down–Strategie. Die Prolog–Auswertungstrategie, der Algorithmus zur Beantwortung von Anfragen, folgt der top–down–Strategie: die Anfrage wird solange mit Regeln und Fakten resolviert, bis die leere Anfrage herauskommt. Da bei Prolog jedoch noch Variablen ins Spiel kommen, m¨ussen wir uns zun¨achst dem systematischen Einsetzen von Variablen, der Unifikation zuwenden.

4.3.1

Unifikation

Wir definieren die Substitution von Variablen in Klauseln analog zur Pr¨adikatenlogik: wenn F eine Klausel ist und t ein Term, so soll F [X/t ] jedes Auftreten von X in F durch t ersetzen. Substitutionen k¨onnen auch verkettet (nacheinander ausgef¨uhrt) werden. Zum Beispiel soll die Substitution folgendermassen wirken: add(X,f(a),f(X))[X/f(f(a))] add(f(f(a)),f(a),f(f(f(a))))  Wenden wir uns noch einmal dem im Einf¨uhrungsabschnitt 4.2 gebrachten Beispiel zu. Dort sollte herausgefunden werden, ob Adam der Grossvater von Claus ist (?-opa(adam,claus)), und dabei die Regel opa(X,Y):-vater(X,Z),vater(Z,Y) angewendet werden. Zu diesem Zweck f¨uhrten wir die Substitution [X/adam][Y/claus] durch, damit in der Anfrage und im Kopf der anzuwendenden Regel das Gleiche steht. Dies nennt man Unifikation: f¨ur zwei Terme soll eine Substitution gefunden werden, nach deren Anwendung die Terme gleich sind. Definition 4.3.1 (Unifikation in Prolog) Zwei Terme F und G heissen unifizierbar, wenn es eine Substitution sub gibt, sodass nach Ausf¨uhrung des Unifikators f¨ur beide das Gleich herauskommt, d.h. G sub

F sub Eine solche Substitution nennt man einen Unifikator.

Beispielsweise sind add(X,a,X) und add(f(B),Z,Y) mittels [X/f(a)][B/a][Y/f(a)][Z/a] unifizierbar (check it!), es kommt beide Male add(f(a),a,f(a)) heraus. Bei genauerem Hinsehen f¨allt auf, dass zu viel substituiert wurde: es war u¨ berhaupt nicht n¨otig, B zu substituieren, die Substitution [X/f(B)][Y/f(B)][Z/a] h¨atte gen¨ugt. Dies ist ein sog. allgemeinster Unifikator, den erstgenannten ¨ Unifikator erh¨alt man durch Nachschalten der Substitution [B/a]. Ahnliches hatten wir schon bei dem einf¨uhrenden Beispiel auf S. 92 gesehen. Ein allgemeinster Unifikator ist, umgangssprachlich gesprochen, ein Unifikator, der so viele Variablen wie m¨oglich unber¨uhrt l¨asst. Jeder andere Unifikator kann dann durch Nachschalten einer weiteren Substitution erhalten werden. Definition 4.3.2 (Allgemeinster Unifikator) Der allgemeinste Unifikator ist ein Unifikator sub von F und G, sodass jeder weitere Unifikator sub von F und G durch Nachschalten einer weiteren Substitution ssub3 erhalten werden kann, d.h. sub sub ssub3 . (Zwei Substitutionen gelten als gleich, wenn sie auf jede Variable angewendet das Gleiche ergeben.) Es gibt aber auch Klauseln, die nicht unifizierbar sind: die Terme vater(X,j¨ urgen) und bruder(johannes,Y) sind ein solcher Fall. Es gibt keine Substitution, die, auf beide Seiten angewendet das Gleiche ergibt, da schon die f¨uhrenden Atome vater und bruder unterschiedlich sind. 99

Schauen wir uns f¨ur eine bessere Intuition noch ein letztes Beispiel an: vater(X,peter) und vater(f(g(Y,a),Z),Z) 

Das f¨uhrende Atom vater stimmt u¨ berein. Doch schon bei einer ersten Inspektion des Inneren der Klauseln f¨allt ins Auge, dass X substituiert werden muss. Es ist sogar notwendig, schon jetzt festzulegen, dass X mit f(g(Y,a),Z) zu substituieren ist, obwohl Y und Z sp¨ater wom¨oglich selbst noch substituiert werden. Diese Einsicht bringt uns zu einem Algorithmus, der zu zwei Strukturen den allgemeinsten Unifikator berechnet, wenn dieser existiert, und failure“ liefert, wenn die zwei Strukturen nicht unfizierbar ” sind. Der Algorithmus wird jeweils versuchen, Paare von Termen zu unifizieren: 4

4

Wenn einer der Terme eine Variable ist, dann wird diese Variable mit dem anderen Term substituiert,

4

Wenn beide Terme Strukturen sind, so muss das f¨uhrende Atom gleich sein. In diesem Fall wird dann versucht, alle Teilterm-Paare zu unifizieren, Wenn beides nicht funktioniert, sind die Terme nicht unifizierbar.

Algorithmus 4.3.3 Input: Zwei Strukturen F und G, die keine Variablen gemeinsam haben. Output: Der allgemeinste Unifikator sub von F und G, sonst failure“. ” Algorithmus: Initialisiere die Substitution sub zur leeren Substitution. Initialisiere eine leere Liste list. ?

H¨ange das Termpaar F G“ vorne an list an. ” while (list ist nicht leer) do ?

H¨ange das erste Termpaar F1 G1“ von list ab. ” if (F1 ist eine Variable X , die im Term G1 nicht vorkommt) Substituiere [X /G1 ] in allen Termpaaren in list. Setze sub : sub [X /G1 ]. if (G1 ist eine Variable X , die im Term F1 nicht vorkommt) Substituiere [X /F1 ] in allen Termpaaren in list. Setze sub : sub [X /F1 ]. if (F1 und G1 sind identische Konstanten oder Variablen) Fahre fort. if (F1

f (F1 ,  ,Fn ) und G1 ?

f (G1 ,  ,Gn ) f¨ur ein Atom f und eine Zahl n)

?

H¨ange F1 G1  Fn Gn “ vorne an list an. ” if (die obigen vier F¨alle sind alle nicht eingetreten) Gib failure“ aus. ” end while Gib den allgemeinsten Unifikator sub aus.

100

Satz 4.3.4 Der Unifikationsalgorithmus ist korrekt, d.h. er findet den allgemeinsten Unifikator zweier Terme F und G, falls dieser existiert, und gibt sonst failure“ aus. Insbesondere gibt es f¨ur zwei unifi” zierbare Terme F und G immer einen allgemeinsten Unifikator. Beweis: Wir erweitern zun¨achst den Begriff Unifikator: Eine Liste von Termpaaren  F1  G1  56 Fk  Gk  heisst unifizierbar, wenn es eine Substitution sub gibt, die alle Paare simultan unifiziert (d.h. Fi sub Gi sub f¨ur alle i). Allgemeinster Unifikator wird analog definiert. Der Algorithmus terminiert, da entweder in jedem Schritt eine Variable aus der Liste verschwindet (die ersten beiden if–Abfragen) oder die Gesamtzahl der Zeichen in der Liste geringer wird (in der dritten und vierten if–Abfrage wird je ein Atom aus der Liste eliminiert). Um die Korrektheit des Algorithmus zu beweisen, wollen wir in jedem Schritt folgende Invariante nachpr¨ufen: Invariante: Bei jedem Test der while-Schleife gilt, dass die Liste list genau dann unifizierbar ist, wenn die Ursprungsterme F und G unifizierbar sind. Weiterhin gilt zu diesen Zeitpunkten f¨ur die aktuelle Substitution sub: Eine Substitution t ist genau dann ein allgemeinster Unifikator von list, wenn sub t ein allgemeinster Unifikator von F und G ist. Die Invariante gilt offenbar bei der Initialisierung: anfangs ist nur F und G in der Liste und die Substitution ist leer. Wenn es uns gelingt zu zeigen, dass die Invariante w¨ahrend jedes Schrittes gilt, dann auch wenn list letztendlich leer ist. In diesem Fall haben wir also einen allgemeinsten Unifikator von F und G gefunden. Und umgekehrt, falls F und G nicht unifizierbar sind, so kann dies nicht passieren, also m¨ussen wir irgendwann in die f¨unfte if–Abfrage hineinlaufen und failure“ ausgeben. ” Behauptung 1. Sei  F1  G1   1 Fk  Gk  eine Liste von Termpaaren, s allgemeinster Unifikator von F1 und G1 sowie s allgemeinster Unifikator der Liste  F2 s  G2 s  77 Fk s  Gk s  , dann ist s s allgemeinster Unifikator der urspr¨unglichen Liste. Dies bedeutet: Um einen allgemeinsten Unifikator einer Liste von Termpaaren zu finden, kann zuerst der allgemeinste Unifikator des ersten Termpaares  F1  G1  berechnet werden, dieser auf den Rest der Liste angewendet und von diesem Rest dann der allgemeinste Unifikator gefunden werden. Wir wollen uns die G¨ultigkeit der Behauptung klarmachen: Da s ein Unifikator von  F1  G1  ist, ist auch s s einer. Da s die Termpaare  Fi s  Gi s  unifiziert, ist s s ein Unifikator der Termpaare  F2  G2  18 Fn  Gn  . Also ist s s ein Unifikator der gesamten Liste. Es bleibt zu zeigen, dass er auch allgemeinster Unifikator ist. Sei also t ein weiterer Unifikator der gesamten Liste. Dann ist t auch ein Unifikator von  F1  G1  , also gibt es nach Definition des allgemeinsten Unifikators s eine Substitution t  , sodass t s t  . Da t auch den Rest der Liste unifiziert, ist t  ein Unifikator der Liste  F2 s  G2 s  8 Fn s  Gn s  . Ebenfalls nach Definition von s gibt es eine Substitution t   , sodass t  s t   . Also ist t s s t   , und s s ist allgemeinster Unifikator. Was tut nun der Algorithmus? Er beginnt damit, die beiden zu unifizierenden Terme F und G in eine Liste zu speichern. Dann nimmt er sich jeweils das erste Termpaar der Liste und versucht, es zu unifizieren. Zun¨achst gibt es drei Regeln, wie mit Variablen oder Konstanten zu verfahren ist, und dann eine Regel, wie mit komplizierteren Strukturen umgegangen wird. Diese werden n¨amlich auf gleiches f¨uhrendes Atom und gleiche Stelligkeit u¨ berpr¨uft. Dann werden alle neuen Termpaare aus den Argumenten gebildet und an die Liste angeh¨angt. Wenden wir uns zun¨achst den ersten beiden Regeln zu. Behauptung 2. Wenn X eine Variable ist, f¨ur die entweder T X gilt oder die im Term T nicht vorkommt, so ist [X /T ] ein allgemeinster Unifikator. Wenn die Variable X hingegen in T vorkommt, so sind X und T nicht unifizierbar. Wenn X nicht in T vorkommt, so ist offenbar sub [X /T ] ein Unifikator der beiden Terme X und T . Warum ist sub aber ein allgemeinster Unifikator? Sei sub ein weiterer Unifikator von X und T . Die 101

Behauptung ist nun, dass sub sub sub . Zwei Substitutionen sind gleich, wenn sie auf jeden Term angewendet das Gleiche ergeben. Es gilt aber X sub T sub , da sub ein Unifikator ist, dies ist aber wegen T X sub gleich X subsub . Damit ist die Behauptung f¨ur die Variable X gezeigt. F¨ur jede weitere Variable bewirken die beiden Substitutionen ebenfalls das Gleiche, da sub keine Ver¨anderung bewirkt. Wenn X nun doch in T vorkommt, so kann es keinen Unifikator geben. Der Term T ist dann l¨anger als X (in Zeichen gerechnet). Weiterhin wird T durch eine Substitution mindestens soviele Zeichen verl¨angert wie X , da X ja in G vorkommt. Also ist T sub immer l¨anger als X sub. Wir wollen die Beweisst¨ucke nun zusammenf¨ugen und einen Durchlauf der while-Schleife durchspielen. Wir haben schon gesehen, dass die Invariante zu Beginn gilt. Wenn die erste if-Abfrage positiv beantwortet wird, so ist wegen Behauptung 2 der allgemeinste Unifikator s von F1 X und G1 T bekannt: s [X /T ]. sub wird auf sub s gesetzt und die Liste list durch  F2 s  G2 s  97 Fn s  Gn s  ersetzt. Wegen Behauptung 1 ist die Invariante immer noch erf¨ullt. Ebenso, wenn die zweite if-Abfrage erf¨ullt wird. Die dritte ber¨uhrt die Invariante trivialerweise nicht. Die Erhaltung der Invariante in der vierten if-Abfrage wird durch die folgende Aussage gezeigt: Behauptung 3. Wenn F1 f (F1 ,  ,Fn ) und G1 f (G1 ,  ,Gn ) (f¨ur gleiches Atom f und gleiche Stelligkeit n) und wenn list  F1  G1  1 F2  G2   Fk  Gk  , dann sind s¨amtliche Termpaare in list genau dann unifizierbar, wenn es die Listen von Termpaaren  F1  G1   Fn  Gn  1 F2  G2   Fk  Gk  sind. Die allgemeinsten Unifikatoren stimmen u¨ berein. Hierzu ist nur zu bemerken, dass F1 s direkt.

f (F1 s,  ,Fn s) ist. F¨ur G1 gilt Analoges. Die Behauptung folgt

Wenn keine der ersten vier if-Abfragen zu einem positiven Ergebnis kommt, liegt das entweder daran, dass einer der Terme F1 und G1 eine Variable ist, der im jeweils anderen Term vorkommt (nicht unifizierbar nach Behauptung 2), dass sie unterschiedliche Konstanten sind (demnach nicht unifizierbar) oder dass sie Strukturen mit unterschiedlichen Atomen oder Stelligkeiten sind (auch nicht unifizierbar).

Wir wollen uns zwei Beispiele anschauen. Hierbei ist in jedem Schritt der Inhalt der Liste list (¨ubereinan-

102

der), die aktuelle Substitution sub sowie die Nummer der n¨achsten angewendeten if–Abfrage angegeben: Erster Term q(g(f(X),Y),g(X,g(Y,Y)))

:?

g(f(X),Y)

Zweiter Term

Substitution if#

q(Z,g(f(a),Z))

[]

:?

4

Z

[]

:?

2

g(X,g(Y,Y))

g(f(a),Z)

g(X,g(Y,Y))

:?

g(f(a),g(f(X),Y))

[Z/g(f(X),Y)]

:?

4

X

f(a)

[Z/g(f(X),Y)]1

g(Y,Y)

:?

g(Y,Y)

:?

g(f(f(a)),Y)

[Z/g(f(X),Y)][X/f(a)]

:?

4

Y

f(f(a))

[Z/g(f(X),Y)][X/f(a)]

1

g(f(X),Y)

Y

:?

f(f(a))

:?

f(f(a))

[Z/g(f(X),Y)][X/f(a)][Y/f(f(a))]

:?

4

f(a)

f(a)

[Z/g(f(X),Y)][X/f(a)][Y/f(f(a))]

:?

4

a

a

[Z/g(f(X),Y)][X/f(a)][Y/f(f(a))]

3

Y

Ausgabe:

[Z/g(f(X),Y)][X/f(a)][Y/f(f(a))]

Erster Term und(name(mein,Z),weiss(Y,Z))

:?

name(mein,Z)

:?

weiss(Y,Z)

:?

mein

:?

Z

:?

Zweiter Term

Substitution if#

und(name(X,hase),weiss(nichts,X))

[]

4

name(X,hase)

[]

4

[]

2

[X/mein]

1

weiss(nichts,X) X hase

weiss(Y,Z)

:?

Z

:?

hase

weiss(Y,Z)

:?

weiss(nicht,mein)

weiss(Y,hase)

:?

weiss(nicht,mein)

[X/mein][Z/hase]

:?

4

Y

nicht

[X/mein][Z/hase]

1

[X/mein][Z/hase][Y/nicht]

5

hase

:?

hase

:?

weiss(nicht,X)

mein mein

Ausgabe:

4.3.2

failure

Resolution

Die Resolution in Prolog ist der Resolution aussagenlogischer Klauseln in KNF sehr a¨ hnlich. Auch in Prolog wird positives und negatives Auftreten einer Struktur (damals einer atomaren Formel) gegeneinander gek¨urzt“. Positiv treten Strukturen nur als Kopf in Regeln und in Fakten auf. Negatives Auftreten ” interessiert uns nur in Anfragen. Um resolvieren zu k¨onnen, m¨ussen wir die in Frage kommenden Strukturen unifizieren, also gleichsam die Regel (den Fakt) und die Anfrage auf einen gleichen“ Nenner ” bringen. Definition 4.3.5 (Resolvierbar, Resolvent) Gegeben seien eine Anfrage und eine Regel (oder ein Fakt) ?-D1 ,  ,Dm .

und

B:-C1 ,  ,Cn . 

die (ggf. nach Umbenennung von Variablen) keine Variablen gemeinsam haben. (Ein Fakt wird als eine Regel ohne Rumpf betrachtet, d.h. n 0.) Diese beiden Klauseln heissen resolvierbar, wenn D1 und B unifizierbar sind. Sei sub der allgemeinste Unifikator von D1 und B. Dann ist ?-C1 sub,  ,Cm sub,D2 sub,  ,Dn sub.

der Resolvent der beiden Formeln. 103

Was heisst das im Beispiel zu Beginn dieses Kapitels? Anfangs stehen sich die folgende Anfrage bzw. Regel gegeben¨uber: ?-opa(adam,claus)

und

opa(X,Y):-vater(X,Z),vater(Z,Y).

Also, n 1, m 2, D1 opa(adam,claus), B opa(X,Y), C1 vater(X,Z) und C2 vater(Z, Y). Der allgemeinste Unifikator von opa(adam,claus) und opa(X,Y) ist sub [X/adam][Y/claus]. Also erhalten wir als Resolvent die Anfrage ?- ; vater(adam,Z) = ?  ; vater(Z,claus) = ? . C1 sub

C2 sub

Beachte, dass die Anfrage in diesem Fall aus nur einem Term bestand (n 1), also die Terme D2  nicht existieren, also einfach nichts hinten angeh¨angt wird. In der n¨achsten Resolution unseres Beispiels ist dies aber nicht mehr so: ?-vater(adam,Z),vater(Z,claus).

und

vater(adam,bertold).

sollen resolviert werden. Der Fakt vater(adam,bertold) passt (mit Hilfe der oben genannten kleinen Syntax¨anderung) durch m 0 in unser Muster der Resolutionen. Wir haben dann also n 2, m 0, D1 vater(adam,Z), D2 vater(Z,claus), B vater(adam,bertold) und fertig: kein C1  Cm . Da der allgemeinste Unifikator sub [Z/bertold] ist, lautet der Resolvent zwingend: ?- ; vater(bertold,claus) = ? . D2 sub

Der Leser u¨ berzeuge sich davon, dass es sich auch beim letzten Schritt unseres Beipiels um eine legale Resolution handelt. Satz 4.3.6 Der Resolvent einer Regel (eines Faktes) und einer Anfrage ist eine Folgerung aus diesen (im pr¨adikatenlogischen Sinne). Konkreter: Gegeben eine Regel (oder ein Fakt) B:-C1 ,  ,Cm . und eine Anfrage ?-D1 ,  ,Dn . Sei sub der allgemeinste Unifikator von B und D1 . Dann ist die pr¨adikatenlogische Entsprechung des Resolventen ?-C1 sub,  ,Cm sub,D2 sub,  ,Dn sub. eine Folgerung aus den pr¨adikatenlogischen Entsprechungen der Regel und der Anfrage. Wie schon in der Aussagenlogik bedeutet dies, dass die Bildung des Resolventen eine algorithmische M¨oglichkeit darstellt, Folgerungen einer gewissen Formelmenge zu generieren. Prolog benutzt diese M¨oglichkeit, um aus dem Programm (den Regeln und Fakten) und einer Anfrage die leere Anfrage zu folgern, also die Anfrage mit Ja“ zu beantworten. Nur wenn dies nicht geht, wird Nein“ ausgegeben. ” ” Beweis: Die Idee ist im Grunde die Gleiche wie bei der aussagenlogischen Resolution, nur die pr¨adikatenlogischen Eigenheiten wie Variablen und Quantoren machen das Argument ein wenig technischer. Die pr¨adikatenlogische Entsprechung der Regel und der Anfrage ist: 

F



x1  xr  B  C1 . Cm  * D1 /% Dn  

wobei x1  xr die in Regel und Anfrage vorkommenden Variablen sind. Durch den allgemeinsten Unifikator werden einige dieser Variablen durch andere Terme ersetzt. Die Formel, die nach dieser Substitution erhalten wird, ist eine Folgerung, da lediglich allquantisierte Variablen durch einen spezielleren Term (oder eine andere allquantisierte Variable) ersetzt werden. F

@

F sub 



x1  xr  B sub  C1 sub .1 Cm sub  * D1 sub / Dn sub 104

Sei nun A ein Modell von F sub. Also muss f¨ur alle Einsetzungen, die man f¨ur x1  xr vornehmen kann,  B sub CBD/E C1 subCBDFG+ Cm subCBH I D1 sub CBI+E D2 subCBDFG+ Dn sub CBE wahr sein. Da nun aber B sub D1 sub, so muss entweder  B sub CB oder  D1 subCB wahr sein. Also muss mindestens einer der Ausdr¨ucke  C1 sub JKL Cm subCB und  D2 sub GL Dn sub CB wahr sein. Das bedeutet aber, dass die Formel 

von A

4.3.3



x1  xr  C1 sub .1 Cm sub * D2 sub / Dn sub

erf¨ullt wird. Diese Formel ist jedoch genau die Entsprechung des Resolventen.

Auswertungsstrategie

Eigentlich ist ja jetzt alles klar. Um f¨ur eine Anfrage zu beantworten, ob sie aus dem Programm folgt, m¨ussen wir nur passende Regeln und Fakten mit ihr resolvieren. Damit dies immer geht, m¨ussen wir wom¨oglich immer wieder Variablenumbenennungen vornehmen (dies geschieht in den Regeln, wir haben schon gesehen, dass dies keinen Einfluss auf das Programm hat). Mit dem Ergebnis (einer neuen Anfrage) f¨ahrt man dann genauso fort. Das ist der top-down-Strategie im Abschnitt 1.21 ganz a¨ hnlich. Wenn wir allerdings einen Algorithmus schreiben wollen, der uns diese Aufgabe abnimmt (einen sogenannter Prolog–Interpreter), so stossen wir schnell darauf, dass es immer noch Auswahlm¨oglichkeiten zwischen mehreren Alternativen geben kann und dass man mit dieser Strategie in Sackgassen laufen kann. Wir werden gleich sehen, wie Prolog mit diesen Problemen umgeht. Betrachten wir das Programm: add(a,X,X). add(f(X),Y,f(Z)):-add(X,Y,Z).

und die Anfrage ?-add(X,Y,f(f(a))) ( Welche nat¨urlichen Zahlen x und y addieren sich zu 2?“) ” Schon im ersten Schritt kann man die Anfrage mit beiden Programm–Klauseln resolvieren: Nach der Resolution mit add(a,X1,X1) (erste Klausel nach Umbenennung der Variablen) erh¨alt man sofort die leere Klausel ?-. und die Ergebnis–Substitution [X/a][Y/f(f(a))], also x 2 und y 0. Wenn man jedoch mit add(f(X1),Y1,f(Z1)):-add(X1,Y1,Z1) resolviert, so erh¨alt man die neue Anfrage ?-add(X1,Y1,f(a)) mit Substitution [X/f(X1)][Y/Y1][Z1/f(a)]. F¨ur die Resolution dieser Anfrage gibt es wieder zwei M¨oglichkeiten und so weiter. Der vollst¨andige Baum aller M¨oglichkeiten sieht dann so aus (man pr¨ufe nach, dass alle L¨osungen f¨ur x  y 2 dabei sind): ?-add(X,Y,f(f(a))). add(a,X1,X1). sub=[X/a][Y/X1][X1/f(f(a)]

add(f(X1),Y1,f(Z1)):-add(X1,Y1,Z1). sub=[X/f(X1)][Y/Y1][Z1/f(a)]

?-.

?-add(X1,Y1,f(a)). add(a,X2,X2). sub=[X1/a][Y1/f(a)][X2/f(a)]

add(f(X2),Y2,f(Z2)):-add(X2,Y2,Z2). sub=[X1/f(X2)][Y1/Y2][Z2/a]

?-.

?-add(X2,Y2,a). add(a,X3,X3). sub=[X2/a][Y2/a][X3/a]

?-.

105

Man nennt diesen Baum aller m¨oglichen Resolutionen auch den Auswertungsbaum des Programms und der Anfrage. Die Auswertungsstrategie sieht nun so aus, dass immer mit der ersten m¨oglichen Regel resolviert wird. Prolog w¨urde also X=a und Y=f(f(a)) ausgeben (ganz linker Ast im Auswertungsbaum). Anders gesagt, im Auswertungsbaum wird in jedem Schritt zum am weitesten links stehenden Kindknoten u¨ bergegangen. Was aber, wenn man durch Anwenden dieser Strategie zu einer Anfrage gelangt, die nicht mit Ja“ zu ” beantworten ist. Beispielsweise k¨onnte man das Programm vater(artur,beat). vater(artur,christoph). vater(christoph,dieter). opa(X,Y):-vater(X,Z),vater(Z,Y).

nach ?-opa(artur,dieter) befragen. Der Auswertungsbaum s¨ahe dann so aus: ?-opa(artur,dieter). opa(X,Y):-vater(X,Z),vater(Z,Y). sub=[X/artur][Y/dieter] ?-vater(artur,Y),vater(Y,dieter). vater(artur,beat). sub=[Z/beat]

vater(artur,christoph). sub=[Z/christoph]

?-vater(beat,dieter).

?-vater(christoph,dieter). vater(christoph,dieter).

PSfrag replacements

M ?-.

Um die Anfrage zu beantworten, w¨urde Prolog die opa–Regel anwenden, und dann vor der Aufgabe stehen, einen Sohn f¨ur artur zu finden, der gleichzeitig Vater von dieter ist. Nach der oben genannten Strategie wird der erste passende Fakt vater(vater,beat) ausgew¨ahlt, das heisst, beat geraten. Dieser Weg entpuppt sich aber als Sackgasse, da beat nicht nachweislich der Vater von dieter ist. Prolog tut dann das einzige, was an dieser Stelle Sinn macht: Es geht einen Schritt zur¨uck, wo es falsch geraten hat und versucht es mit der n¨achsten anwendbaren Regel. Dieser R¨uck–Schritt wird Backtracking genannt. Genau gesagt bedeutet es: Wenn Prolog zu einer Anfrage entweder keine Regeln und keine Fakten zum Unifizieren findet oder alle gefundenen Resolventen mit nicht ableitbar“ zur¨uckgegeben werden, gibt es ” auch f¨ur diese Anfrage nicht ableitbar“ zur¨uck. Der ganze Auswertungsalgorithmus sieht so aus: ” Algorithmus 4.3.7 Bi :-Ci  1 ,  ,Ci  ni . Regeln und Fakten, Input: Prolog–Programm F K1 K2  Kn , wobei Ki und Zielklausel G ?-A1 ,  ,Ak . Output: wenn die Zielklausel ableitbar ist, die Substitution sub und Ja“, sonst Nein“. ” ” Algorithmus: Hauptprogramm Setze globale Variable erfolg : falsch. Setze sub : leere Substitution. F¨uhre die Prozedur auswerte  G  sub  aus. 106

if erfolg then Schreibe Ja“. ” else Schreibe Nein“. ” Ende Hauptprogramm. Prozedur auswerte (Zielklausel G if G ist die leere Anfrage (d.h. m Schreibe sub. Setze erfolg : wahr.

?-D1 ,  ,Dm ., Substitution sub)

0 und G

?-.) then

else Setze i : 0. while i N n and not erfolg do Setze i : i  1. if O D1  Bi P ist unifizierbar mit allgemeinstem Unifikator sub (wobei Variablen in Ki evtl. umbenannt wurden) then F¨uhre auswerte( ?-Ci  1 sub ,  ,Ci  ni sub ,D2 sub ,  ,Dm sub .“, sub sub ) aus. ” Ende Prozedur auswerte. Um den Algorithmus in aller Tiefe zu erkl¨aren, wenden wir uns wieder dem Beispiel zu (es empfiehlt sich, den Fluss des Algorithmus im Auswertungsbaum oben zu verfolgen): Zu Beginn ist die globale Variable erfolg auf falsch gesetzt. Die Substitution sub, in der alle anfallenden Ersetzungen gesammelt werden, ist noch leer. Nun wird die Prozedur auswerte mit G ?-opa(artur,dieter) und dieser leeren Substitution aufgerufen. Diese Zielklausel G ist nicht leer, also wird in den else–Teil gesprungen. Die erste Struktur in der Zielklausel ist zugleich auch die einzige, also D1 opa(artur,dieter). In der while–Schleife ist zun¨achst f¨ur i 1, also B1 vater(artur,beat). Dies ist nat¨urlich nicht mit D1 unifizierbar, also geht es gleich weiter mit B2 vater(artur,christoph) . Auch diese Struktur ist nicht mit D1 unifizierbar, also weiter zu B3 und, weil auch nicht unifizierbar, zu B4 . Hier werden wir endlich f¨undig, der allgemeinste Unifikator lautet sub [X/artur][Y/dieter] , und die Prozedur auswerte wird nun noch einmal aufgerufen. Da die Regel K4 opa(X,Y):-vater(X,Z),vater(Z,Y) nur zwei Strukturen C4  1 vater(X,Z) und C4  2 vater(Z,Y) und die Anfrage ausser D1 gar keine Strukturen enth¨alt, ist die neue Zielklausel (der Resolvent) gerade ?-C4  1 [X/artur][Y/dieter],C4  2 [X/artur][Y/dieter]

?-vater(artur,Z),vater(Z,dieter) 

All dies bringt uns im Auswertungsbaum eine Etage tiefer. Wir haben gesehen, dass nacheinander alle Klauseln (Regeln und Fakten) des Prolog–Programms durchlaufen werden und versucht wird, den ersten Regelkopf oder Fakt zu finden, der mit der ersten Struktur der aktuellen Zielklausel unifizierbar ist. Wir wollen uns nun verdeutlichen, wie es weiter geht. Wieder ist die Zielklausel G

?-vater(artur,Z),vater(Z,dieter)

nicht leer, sie ist aber gleich mit dem ersten Fakt des Programms K1 vater(artur,beat) unifizierbar. Der allgemeinste Unifikator ist [Z/beat]. Der Resolvent lautet vater(beat,dieter), die Prozedur auswerte wird erneut aufgerufen. Diesmal ist aber weder die Zielklausel leer noch gibt es einen Fakt oder eine Regel, mit der sie resolvierbar ist. Also wird die Prozedur auswerte unverrichteter Dinge verlassen und wir sind wieder in der while–Schleife der Prozedur auswerte eine Etage h¨oher. Der n¨achste Fakt K2 vater(artur,christoph) ist mit der Zielklausel resolvierbar (allg. Unifikator [Z/christoph]), 107

der Resolvent lautet ?-vater(christoph,dieter). Die Prozedur auswerte wird mit dieser Zielklausel und der Substitution sub [X/artur][Y/dieter][Z/christoph] aufgerufen. Resolviert wird erst mit dem dritten Fakt vater(christoph,dieter). Der neue Resolvent ist die leere Klausel ?-. Dies wird auch im erneuten Aufruf auswerte erkannt. Die Substitution wird ausgegeben. (Prolog schreibt nur die Substitutionen, die schon in der urspr¨unglichen Zielklausel vorkamen, in unserem Fall wird also nichts ausgegeben. Es ist aber prinzipiell m¨oglich, die Substitutionen aller Variablen auszuschreiben.) Nun wird die globale Variable auf wahr gesetzt und die Prozedur verlassen. Wieder sind wir in der while–Schleife eine Etage h¨oher, doch not erfolg falsch, also wird sie verlassen und noch eine Etage h¨oher gesprungen. Das geht so weiter, man hangelt sich den Auswertungsbaum bis zum Hauptprogramm hoch. Letztendlich wird erfolg getestet und Ja“ ” ausgegeben. Wir wollen noch einmal die Arbeitsweise des Algorithmus zusammenfassen: Der Auswertungsbaum wird nach einer leeren Klausel durchsucht, dabei wird immer die erste passende Programmklausel zum resolvieren gesucht und mit dem Resolventen weitergemacht. Wenn an einer Stelle keine Programmklausel zum resolvieren gefunden werden kann, dann wird zur aufrufenden Prozedur auswerte gesprungen und dort der n¨achste passende Resolvent gesucht. Wenn an einer Stelle die leere Anfrage gefunden wird, gibt Prolog die aktuelle Substitution aus und hangelt sich den Baum bis ganz an die Wurzel, das Hauptprogramm, hoch und gibt Ja“ aus. Wenn dies nie der Fall ist, aber der Baum trotzdem ganz durchlaufen ” wurde, dann ist erfolg nach wie vor falsch, also gibt Prolog Nein“ aus, die Anfrage ist nicht ableitbar. ” Um sich die Auswertung einer Anfrage kurz zu verdeutlichen, kann man auch lediglich die Aufrufe der Prozedur auswerte incl. der aktuellen Zielklausel und der aktuellen Substitution notieren. Die Tiefe der Einr¨uckung beschreibt dabei die Tiefe des Auswertungsbaums. auswerte (?-opa(artur,dieter), []) auswerte (?-vater(artur,Z),vater(Z,dieter), [X/artur][Y/dieter]) auswerte (?-vater(beat,dieter), [X/artur][Y/dieter][Z/beat]) auswerte (?-vater(christoph,dieter), [X/artur][Y/dieter][Z/christoph]) auswerte (?-., [X/artur][Y/dieter][Z/christoph])

108

4.4

Programmieren in reinem Prolog

Die beste (und nach Meinung der Autoren einzig richtige und gr¨undliche) Art und Weise, Prolog zu lernen, ist es, viele Beispiele zu rechnen. In diesem Kapitel wollen wir eine erste Anleitung dazu geben, wie man Prolog–Programme verfasst. Dabei wird auch ein neues Denken u¨ ber Prolog eine Rolle spielen: eine doch wieder eher prozedurale Sichtweise. Ab einem bestimmten Punkt beginnt man, die Arbeitsweise der Auswertungsstrategie von Prolog so sehr auszunutzen, dass jede Abweichung von dieser Strategie zu einem unsinnigen Ergebnis f¨uhren w¨urde. Hat man beispielsweise eine Regel A:-B1 ,  ,Bn gegeben, so kann man dies auch als Um (Prozedur) A erfolgreich abzuarbeiten, m¨ussen wir erst B1  Bn in genau dieser Reihenfolge ab” arbeiten“. Kommt man dann an einer Stelle zur Anfrage ?-A,D2 ,  ,Dm , so heisst das Arbeite zuerst ” A ab, dann D2  Dm in dieser Reihenfolge“. Die zu unifizierenden Variablen k¨onnen dann wie Eingabe zu einer Prozedur wirken. Im Unterschied zu wirklich prozeduralen Programmiersprachen haben aber diese Variablen keinen Typ. Sie k¨onnen auch, einmal unifiziert, nicht nachtr¨aglich ge¨andert werden. Diese Sichtweise soll Gegenstand der n¨achsten drei Abschnitte sein.

4.4.1

Rekursion mit Verwandtschaftsbeziehungen

Eingangs wurde schon erw¨ahnt, dass das Konzept der Rekursion eine eminente Rolle bei der Prolog– Programmierung spielt. Wir wollen hier einige kleine Beispiele mit Verwandtschaftsbeziehungen bringen. Es sei eine Menge von Vaterschaftsbeziehungen a¨ hnlich wie im Eingangsbeispiel in diesem Kapitel gegeben. Ein Vorfahr X eines Menschen Y sei ein Mensch, der direkt durch wiederholte Vaterbeziehungen mit Y verwandt ist. Um dies rekursiv zu beschreiben, ben¨otigen wir einen Rekursionsanfang und einen Rekursionsschritt. Der Rekursionsanfang ist ein Basisfall, wo die L¨osung der Anfrage sehr einfach ist. Der Rekursionsschritt sollte von einer Anfrage auf eine neue Anfrage f¨uhren, die n¨aher am Rekursionsanfang (wenn die Anfrage ableitbar ist) oder der Nichtunifizierbarkeit (wenn nicht ableitbar) ist. Im Vorfahrenbeispiel kann man sagen, dass, X klarerweise der Vorfahr von Y ist, wenn X der Vater von Y ist. In Prolog w¨urde man also vorfahr(X,Y):-vater(X,Y) schreiben. Dieser Rekursionsanfang f¨angt nur vorfahr–Beziehungen auf, wo die Vaterschaftskette zwischen X und Y gerade eins lang ist. Der Rekursionsschritt soll diese Kette um eins verk¨urzen. Betrachte die Regel: vorfahr(X,Y):-vater(X,Z),vorfahr(Z,Y).

Der Leser sollte jetzt schon erkennen, was sie bedeutet: X ist ein Vorfahr von Y, wenn es einen Men” schen Z gibt, dessen Vater X ist und der selber ein Vorfahr von Y ist.“ Dies ist offensichtlich wahr. Wie diese Bedingung die Kette von Vaterschaftsbeziehungen verk¨urzen kann, soll am Beispiel des folgenden Programms gezeigt werden: vater(a,b). vater(b,c). vater(b,d). vater(d,e). vorfahr(X,Y):-vater(X,Y). vorfahr(X,Y):-vater(X,Z),vorfahr(Z,Y).

109

Die Anfrage soll ?-vorfahr(a,e). lauten. Die Auswertungsstrategie ruft die Prozedur auswerte mit den folgenden Anfragen auf. Die Darstellung als Baum soll verdeutlichen, von welchem Aufruf der Prozedur auswerte diese erneut aufgerufen wird. Der Baum ist also gleichsam der Auswertungsbaum, abgeschnitten an der Stelle, wo die leere Anfrage gefunden wird. ?-vorfahr(a,e). ?-vater(a,e).

M

?-vater(a,Z1),vorfahr(Z1,e). ?-vorfahr(b,e). ?-vater(b,e).

M

?-vater(b,Z2),vorfahr(Z2,e). ?-vorfahr(c,e).

M

?-vater(c,e).

M

?-vater(c,Z3),vorfahr(Z3,e).

M

?-vorfahr(d,e). ?-vater(d,e).

PSfrag replacements

?-.

Es ist aber auch wichtig, dass ein Pr¨adikat nicht Ja“ antwortet, wenn es eigentlich Nein“ sagen sollte. ” ” F¨ur unseren Fall k¨onnten wir nach ?-vorfahr(c,e) fragen. Man erh¨alt folgenden Auswertungsbaum: ?-vorfahr(c,e). ?-vater(c,e).

PSfrag replacements

M M

?-vater(c,Z1),vorfahr(Z1,e).

M

Wir wollen nun zeigen, dass es sehr darauf ankommt, in welcher Reihenfolge wir die Hypothesen einer Regel schreiben. Wenn wir die letzte Zeile durch vorfahr(X,Y):-vorfahr(Z,Y),vater(X,Z). ersetzen, so ergibt sich bei der Anfrage ?-vorfahr(c,e) folgender Auswertungsbaum: ?-vorfahr(c,e). ?-vater(c,e).

M

?-vorfahr(Z1,e),vater(c,Z1). ?-vater(c,e).

M

?-vorfahr(Z2,e),vater(Z1,Z2),vater(c,Z1). ?-vater(Z1,c),vater(c,Z1).

M

?-vater(c,b).

M

?-vorfahr(Z3,e),vater(Z2,Z3),vater(Z1,Z2),vater(c,Z1).

PSfrag replacements

Es ist schon nach wenigen Schritten ersichtlich, dass der Auswertungsbaum unendlich gross wird, und der Auswertungsalgorithmus in eine unendliche Rekursion l¨auft. Und alles nur wegen falscher Reihenfolge. 110

Es kommt auch vor, dass die Reihenfolge der Klauseln im Programm eine Rolle spielt (nat¨urlich nur von Klauseln mit gleichem f¨uhrenden Atom im Kopf). Ein Beispiel ist das folgende Programm: verheiratet(gabi,klaus). verheiratet(X,Y):-verheiratet(Y,X).

Die Regel in der zweiten Zeile soll bewirken, dass die Anfrage ?-verheiratet(klaus,gabi) trotzdem zu Ja“ evaluiert (Verheiratetsein ist eine beiderseitige Angelegenheit). Und das klappt auch sehr gut: ” ?-verheiratet(klaus,gabi). ?-verheiratet(gabi,klaus). ?-.

Beim zweiten Aufruf von auswerte wird ?-verheiratet(gabi,klaus) mit dem Fakt in der ersten ¨ Zeile resolviert. Ubrig bleibt die leere Anfrage. Vertauscht man jedoch die erste und die zweite Zeile des Programms, so ergibt sich folgender Auswertungsbaum: ?-verheiratet(klaus,gabi). ?-verheiratet(gabi,klaus). ?-verheiratet(klaus,gabi). ?-verheiratet(gabi,klaus).

Es wird in jedem Aufruf die Anfrage mit der Regel resolviert, was die gleiche Anfrage mit vertauschten Argumenten zur Folge hat. Dies bringt uns wieder in eine endlose Rekursion. ¨ Es wird langsam klar, dass naives Ubersetzen einer pr¨adikatenlogischen Regel in Prolog–Klauseln nicht immer zum Ziel f¨uhrt. Man sollte sich beim Programmieren in Prolog stets von der Lauff¨ahigkeit in allen Situationen u¨ berzeugen. Daf¨ur sollten f¨ur jede Regel stets einige (auch unwahrscheinlich erscheinende) Anfragen im Kopf (oder am Computer) durchgetestet werden.

4.4.2

Arithmetik

Wir hatten schon die Addition in nat¨urlichen Zahlen mit Hilfe der folgenden beiden Klauseln modelliert: add(a,X,X). add(f(X),Y,f(Z)):-add(X,Y,Z).

Diese beiden Klauseln verdeutlichen sehr sch¨on das Prinzip der Rekursion: die erste ist der Rekursionsanfang, ein einfacher Basisfall (0  x x), und die zweite ist der Rekursionsschritt, der eine nicht schon dem Rekursionsanfang erliegende Anfrage einfacher machen soll. Beispiel: x 2  3. ?-add(f(f(a)),f(f(f(a))),X). ?-add(f(a),f(f(f(a))),Z1). ?-add(a,f(f(f(a))),Z2). ?-.

[X/f(Z1)] [X/f(Z1)][Z1/f(Z2)] [X/f(Z1)][Z1/f(Z2)][Z2/f(f(f(a)))]

111

Hinter den aktuellen Anfragen haben wir immer die f¨ur die Variable X wesentlichen Substitutionen festgehalten. Es ergibt sich X f(f(f(f(f(a))))), was der richtigen Antwort 5 entspricht. Doch diese beiden Klauseln k¨onnen auch subtrahieren: ?-add(X,f(f(a)),f(f(f(f(a))))). ?-add(X,f(f(a)),f(f(f(f(a))))). ?-add(X1,f(f(a)),f(f(f(a)))). ?-add(X2,f(f(a)),f(f(a))). ?-.

[X/f(X1)] [X/f(X1)][X1/f(X2)] [X/f(Z1)][Z1/f(Z2)][Z2/a]

Auch hier wird zun¨achst zweimal der Rekursionsschritt und dann der Rekursionsanfang zum resolvieren verwendet. Wenn wir in einem Prolog–Programm ein Pr¨adikat subtract benutzen wollen, so k¨onnen wir es uns demnach durch subtract(X,Y,Z):-add(Y,Z,X). ¨ definieren. Bei einem Aufruf der Klausel ?-subtract(f(f(f(f(a)))),f(f(a)),X) wird sich Ahliches ereignen wie zuvor (nachpr¨ufen!). Nach Addition und Subtraktion ist nat¨urlich die Multiplikation als n¨achstes an der Reihe. Wir hatten im Zuge der Modellierung der nat¨urlichen Zahlen in der Pr¨adikatenlogik schon gesehen, welche beiden Regeln gen¨ugen, um die Multiplikation vollst¨andig festzulegen: 0 Q x 0 und  x  1  y xy  y. Die erstere ist wieder der leichte Rekursionsanfang: mult(a,X,a). F¨ur die zweite m¨ussen wir uns ein wenig anstrengen, da wir kein Gleichpr¨adikat haben. Nichtsdestoweniger m¨ussen gleiche Variablen in einer Klausel auch nach Substitutionen den gleichen Wert haben. Wir werden sehen, dass die folgende Regel das Gew¨unschte leistet: mult(f(X),Y,Z):-mult(X,Y,W),add(W,Y,Z).

(Um  x  1  y zu berechnen, errechne zun¨achst w xy, das Ergebnis ist dann w  y. Wieder ist das Errechnen von W einfacher als das Errechnen von Z, da ja f(X) um eins kleiner ist als X. Jedes Anwenden des Rekursionsschrittes wird uns dem Rekursionsanfang einen Schritt n¨aher bringen.) Hier werden die Auswertungsb¨aume schon bei kleinen Anfragen un¨ubersichtlich. Man sollte daher zur Probe nicht wild drauflosrechnen, sondern vielmehr versuchen, zu verstehen, wie sich diese Regeln in den verschiedenen vorstellbaren F¨allen verhalten: auf die Anfrage ?-mult(f(f(f(a))),f(f(a)),X) wird zun¨achst der Rekursionsschritt des mult–Pr¨adikats verwendet. Dies ergibt wieder eine Anfrage mit mult als ersten Teil. Allerdings ist dessen erstes Argument nur noch f(f(a)). In dieser Art und Weise wird der Rekursionsschritt noch zweimal angewendet, bis die Anfrage so aussieht: ?-mult(a,f(f(a)),W3),add(W3,f(f(a)),W2),add(W2,f(f(a)),W1),add(W1,f(f(a)),X).

Nun wird der Rekursionsanfang des mult–Pr¨adikats verwendet, es bleiben die drei add–Pr¨adikate u¨ brig. Doch wir haben schon gesehen, dass diese sehr gut zum Addieren taugen. Da mehrere Anfragen immer von vorne abgearbeitet werden, eine nach der anderen, kommt nacheinander W3 a, W2 f(f(a)), W1 f(f(f(f(a)))) und X f(f(f(f(f(f(a)))))) (d.h. 6) heraus. Wichtig f¨ur die Programmierung mit Zahlen sind nat¨urlich Vergleiche. Wir wollen ein kleiner–Pr¨adikat definieren. Wieder hat der Rekursionsanfang etwas mit der Null und der Rekursionsschritt mit der Nachfolgerfunktion zu tun: kleiner(a,f(X)). kleiner(f(X),f(Y)):-kleiner(X,Y).

112

Der Leser sollte f¨ur sich selbst u¨ bersetzen, was diese Klauseln bedeuten. Wir wollen uns anschauen, was Prolog auf die Anfrage ?-kleiner(f(f(a)),f(f(f(a)))). hin tut: ?-kleiner(f(f(a)),f(f(f(a)))). ?-kleiner(f(a),f(f(a))). ?-kleiner(a),f(a)). ?-.

Sehr gut: Yes“. Was passiert jedoch bei der Anfrage ?-kleiner(f(f(f(f(a)))),f(f(a)))? ” ?-kleiner(f(f(f(f(a)))),f(f(a))). ?-kleiner(f(f(f(a))),f(a)). ?-kleiner(f(f(a)),a).

Der Auswertungsbaum ist vollst¨andig, und doch wurde keine leere Anfrage gefunden, der Auswertungsalgorithmus muss daher Nein“ ausgeben. Dies ist nat¨urlich auch richtig, da eben nicht 4 N 2. ” Basierend auf diesen Basispr¨adikaten kann man weitere Pr¨adikate definieren. Letztes Beispiel an dieser Stelle sollte das Pr¨adikat exp(N,M,X) stehen, welches genau dann wahr wird, wenn nm x. Der Rekursionsschritt sollte den Exponenten kleiner machen: nm R 1 nm Q n, also exp(N,f(M),X):-exp(N,M,Y),mult(Y,N,X).

Der Rekursionsanfang ist nat¨urlich n0 1. Doch stimmt dies in jedem Fall? Was ist, wenn n 0. Dann ist f¨ur m S 1 gerade nm 0m 0, die Potenz 00 ist nicht einmal definiert. Der Ausweg aus dieser Misere ist, den Rekursionsanfang nur f¨ur n T 0 zu definieren: exp(f(N),a,f(a)).

d.h.  n  1 

0

1 f¨ur alle n. F¨ur n

0 haben wir dann noch den Fakt: exp(a,f(M),a).

Der Leser sollte an dieser Stelle versuchen, weitere Pr¨adikate zu definieren, die auf add, mult und kleiner basieren. Denkbar w¨aren zum Beispiel die Fakult¨at fakultaet(N,X) (x n!) oder das Modulo– Pr¨adikat mod(N,M,R) (r ist der Rest bei ganzzahliger Division von n durch m) oder die n–te Fibonacci– f1 1, fn R 2 fn R 1  fn f¨ur alle Zahl fib(N,X) (sie ist von Natur aus schon rekursiv definiert: f0 n).

4.4.3

Listen

Listen sind in Prolog ein essentielles Werkzeug. Weitere Datenstrukturen wie Zeichenketten, B¨aume und Stacks werden alle durch Listen modelliert. Es gibt zwei Konstruktionselemente: die leere Liste nil (Konstante) und den Listenkonstruktor cons (zweistellige Funktion). Dieser Listenkonstruktor hat als ersten Eintrag das erste Listenelement (der sogenannte head) und als zweiten Eintrag den gesamten Rest der Liste (der sogenannte tail). Es ist essentiell zu beachten, dass es sich bei nil und cons nicht um Pr¨adikate handelt. Vielmehr werden diese in Pr¨adikate (wie append – hinten anh¨angen, reverse – Liste umkehren, usw.) eingesetzt.

113

Ein Beispiel: Die Liste, die aus den Elementen 1, 2 und 3 besteht, wird in Prolog als cons(f(a),cons(f(f(a)),cons(f(f(f(a))),nil)))

kodiert. Wir wollen uns dieses Konstrukt von innen nach aussen zerpfl¨ucken. Ganz innen steht cons(f(f(f(a))),nil). Dies ist laut Vereinbarung die Liste, die als erstes Element die 3 und als Rest der Liste die leere Liste hat. Demnach ist dies die Liste mit einem einzigen Element, n¨amlich 3. Nun wird auch klar, dass cons(f(f(a)),cons(f(f(f(a))),nil)) die Liste mit 2 als erstem Element ist, deren Restliste nur aus dem Element 3 besteht. Wir schreiben dies als [2,3]. Wenn wir vor diese Liste noch die 1 h¨angen, so kommt die Liste [1,2,3] heraus. Warum so umst¨andlich? Prolog hat eine eingebaute Vorliebe f¨ur Rekursionen. Die Listendefinition ist nun auch schon potentiell rekursiv. Ein Pr¨adikat list, welches wahr wird, wenn das Argument eine Liste ist, k¨onnte n¨amlich wie folgt definiert werden: list(nil). list(cons(X,Xs)):-list(Xs).

Die leere Liste ist eine Liste und cons(X,Xs) ist eine Liste, wenn Xs schon eine Liste ist. Herrlich rekursiv. Der Ableitungsbaum der Anfrage ?-list(cons(f(a),cons(f(f(a)),nil))) spiegelt wieder, wie zun¨achst zweimal der Rekursionsschritt und dann der Rekursionsanfang angewendet werden, um auf die Antwort Ja“ zu kommen: ” ?-list(cons(f(a),cons(f(f(a))).

list(cons(X1,Xs1)):-list(Xs1).

[X1/f(a)][Xs1/cons(f(f(a)),nil)]

?-list(cons(f(f(a)),nil).

list(cons(X2,Xs2)):-list(Xs2). [X2/f(f(a))][Xs2/nil]

?-list(nil).

list(nil).

?-.

Eine Verkettung von Listen wird durch das append–Pr¨adikat erreicht: append(nil,X,X). append(cons(X,Xs),Ys,cons(Z,Zs)):-append(Xs,Ys,Zs).

Eine Liste, die an eine leere Liste angeh¨angt wird, bleibt gleich.“ und Wenn Xs und Ys zusammen Zs ” ” ergeben, so ergibt die Verkettung von cons(X,Xs) und Ys gerade cons(X,Zs)“. Auch hier spielt wieder die Rekursivit¨at die entscheidenden Rolle. Um zwei Listen zu verketten, wird zun¨achst das erste Element der ersten Liste abgenommen und rekursiv die Restlisten verkettet. An die Ergebnisliste wird dann vorne das abgenommene Element wieder angeh¨angt. Der Rekursionsanfang tritt dann ein, wenn die erste Liste leer ist. Es folgt ein Beispielauswertungsbaum zur Verkettung der Listen [a,b] und [b,c]: ?-append(cons(a,cons(b,nil)),cons(b,cons(c,nil)),X). ?-append(cons(b,nil),cons(b,cons(c,nil)),Z1). ?-append(nil,cons(b,cons(c,nil)),Z2). ?-.

[X/cons(a,Z1)] [Z1/cons(b,Z2)] [Z2/cons(b,cons(c,nil))]

114

Der Wert von X ist nach der Ausf¨uhrung aller Substitutionen cons(a,cons(b,cons(b,cons(c,nil)))) [a,b,b,c]. Doch dieses Pr¨adikat kann noch mehr. Wir k¨onnen fragen, ob eine Liste der Beginn einer anderen Liste ist: ?-append(cons(a,cons(b,nil)),X,cons(a,cons(b,cons(c,nil)))). ?-append(cons(b,nil),X,cons(b,cons(c,nil))). ?-append(nil,X,cons(c,nil)). ?-.

[X/cons(c,nil)]

oder auch, ob eine Liste das Ende einer anderen Liste ist: ?-append(X,cons(c,nil),cons(a,cons(b,cons(c,nil)))). ?-append(X1,cons(c,nil),cons(b,cons(c,nil))). ?-append(X2,cons(c,nil),cons(c,nil)). ?-.

[X/cons(a,X1)] [X1/cons(b,X2)] [X2/nil]

Darum macht es auch Sinn, die Pr¨adikate pr¨ afix und suffix wie folgt zu definieren: pr¨ afix(L1,L2):-append(L1,X,L2). suffix(L1,L2):-append(X,L1,L2).

4.5

Prolog Crashkurs

Das von uns beschriebene reine Prolog (pure prolog) ist theoretisch eine voll entwickelte Programmiersprache, jedoch f¨ur praktische Zwecke zu unhandlich und langsam. Zum Beispiel ben¨otigt die Addition zweier Zahlen x und y proportional zu x viele Programmschritte. Dies ist inakzeptabel, da es eigentlich viel schneller ginge. Wir werden vorstellen, wie eine echte Prolog–Implementierung die Effizienzsteigerung erreicht. Ausserdem werden wir sogenannte Seiteneffekte vorstellen. Dies sind Pr¨adikate, die, wenn sie resolviert werden sollen, Eingaben von der Tastatur oder dem Filesystem bzw. Ausgaben auf den Bildschirm und auf das Filesystem vornehmen. Wir werden den Cut kennenlernen und mit ihm ein not–Pr¨adikat definieren. Ausserdem werden uns sogenannte metalogische Pr¨adikate interessieren.

4.5.1

Prolog am Computer

Wenn man Prolog aufruft, zum Beispiel SWI–Prolog mit Hilfe des Kommandos pl, so bekommt man einen Prompt, n¨amlich das ?-, und Prolog wartet auf eine Anfrage. Anfangs ist noch kein Prolog– Programm geladen, wir den Quellcode vorher in irgendeinem File (z.B. beispiel.pl) gespeichert haben. Man kompiliert dieses File so: ?-consult(beispiel).

(Das ?- muss nicht mehr eingegeben werden, der Punkt ist aber essentiell.) Angenommen, im File beispiel.pl stehen die folgenden Regeln: add(a,X,X). add(f(X),Y,f(Z)):-add(X,Y,Z).

115

Wenn wir die Anfrage ?-add(f(f(a)),f(f(f(a))),X) stellen, das haben wir schon gesehen, kommt die Antwort X=f(f(f(f(f(a))))). Wir quittieren die Antwort mit dem Dr¨ucken der Entertaste und Prolog sagt Yes“. ” Auf die Anfrage ?-add(X,Y,f(f(a))) antwortet Prolog mit X = a, Y = f(f(a)). Aber damit ist der Auswertungsbaum gar nicht ganz abgesucht, sondern nur bis zum ersten Auffinden der leeren Klausel. Es gibt noch zwei weitere Wege (siehe Abschnitt 4.3.3). Um diese zu finden, dr¨ucken wir das Semikolon. Prolog antwortet mit X = f(a), Y = f(a). Was ist passiert? Da der Auswertungsbaum noch nicht vollst¨andig abgesucht war, stoppte die Inferenzmaschine den Auswertungsalgorithmus beim Auffinden der leeren Klausel, aber nach Eingabe des Semikolons wurde die globale Variable erfolg wieder auf false gesetzt. Damit ging die Suche im Auswertungsbaum weiter und wir kamen zur zweiten L¨osung. Nochmaliges Dr¨ucken des Semikolons f¨uhrt zur dritten L¨osung. Nun ist der Auswertungsbaum vollst¨andig abgesucht. Nach nochmaligem Dr¨ucken des Semikolons muss Prolog mit No“ antworten: Nein, es gibt ” keine weiteren L¨osungen. Auf diese Art ist es uns m¨oglich, den gesamten Auswertungsbaum abzusuchen, selbst, wenn wir schon eine leere Klausel gefunden haben. Selbstverst¨andlich steht es jedesmal, wenn Prolog auf eine Quittierung wartet, frei, Enter zu dr¨ucken und die Abarbeitung der Anfrage zu beenden.

4.5.2

Der Unifikationsoperator =

Die Syntax von Prolog wird erweitert: Wenn A und B Terme sind, so soll auch A=B eine Struktur sein. Diese Struktur bedeutet, dass A und B unifizierbar sind. Wenn die Inferenzmaschine auf diese Struktur als Zielklausel st¨osst, so wird genau die Unifikation ausgef¨uhrt. Man kann sich diesen Operator wie ein Pr¨adikat unif vorstellen, welches einfach als unif(X,X).

definiert ist.

4.5.3

Arithmetik

Bisher waren die Argumente von Fakten und Regeln nur Variablen und Strukturen. Doch auch Zahlen sind m¨oglich. Syntaktisch gelten sie als Atome. Wir werden jedoch gleich sehen, wie man mit ihnen rechnet. Daf¨ur gibt es die arithmetischen Operatoren: X+Y, X-Y, -X, X*Y, X/Y, X mod Y, abs(X), sqrt(X), sin(X), cos(X) etc. All diese Ausdr¨ucke gelten jetzt auch als Strukturen. Man muss aber beachten, dass diese Ausdr¨ucke nicht sofort evaluiert werden, sondern als Ausdr¨ucke stehen bleiben. Dies bedeutet beispielsweise, dass die Anfrage ?-X=3+4 die Antwort X=3+4 und nicht etwa X=7 ergibt. Zur Evaluation gibt es den is–Operator: Die Anfrage ?-X is 3+4*2 ergibt gerade X=11. Erst wenn in einer Klausel der is–Operator erreicht wird, werden die arithmetischen Operationen ausgef¨uhrt. Dies ist von Bedeutung, wenn man zum Beispiel ein Pr¨adikat zur Berechnung der Fakult¨at n! 1 Q 2 QQQ n definieren will: fak(0,1). fak(N,X):-N’ is N-1, fak(N’,X’), X is X’*N.

Schauen wir uns den Auswertungsbaum zur Anfrage ?-fak(2,X) an:

116

?-fak(2,X). ?-N1 is 2-1, fak(N1,X1), X is X1*2. ?-fak(1,X1), X is X1*2.

[N1/1]

?-N2 is 1-1, fak(N2,X2), X1 is X2*1, X is X1*2. ?-fak(0,X2), X1 is X2*1, X is X1*2.

[N2/0]

?-X1 is 1*1, X is X1*2.

[X2/0]

?-X is 1*2.

[X1/1]

?-.

[X/2]

An diesem Beispiel sieht man sehr sch¨on, dass die arithmetischen Operatoren zun¨achst nicht, sondern erst beim is–Operator, ausgerechnet werden. Man darf zum Beispiel auch nicht den Fehler machen, statt der obigen Regel die Regel fak(N,X):-fak(N-1,X’),X is X’*N. zu verwenden. Das N-1 w¨urde nicht evaluiert werden und die Auswertung in eine Endlosrekursion laufen: ?-fak(2,X). ?-fak(2-1,X1), X is X1*2. ?-fak(2-1-1,X1), X is X1*2. ?-fak(2-1-1-1,X1), X is X1*2. ?-fak(2-1-1-1-1,X1), X is X1*2.

Auch arithmetische Vergleiche wie X>Y, X=Y, X= 0, write(’Gib das ’), write(M), write(’te Register ein: ’), liesZahl(X), N1 is N - 1, M1 is M + 1, liesRegister(N1, M1, Reg). liesRegister(0, _, []).

%%%%%%%%%% parsing %%%%%%%%%%%%%%%% ziffer([Z|S0], S0, N) :Z >= 48, Z =< 57, N is Z - 48. zahl(S0, S1, SchonDa, Ergebnis) :ziffer(S0, S2, ErsteStelle), JetztDa is SchonDa * 10 + ErsteStelle, zahl(S2, S1, JetztDa, Ergebnis). zahl(S0, S0, Ergebnis, Ergebnis) :not ziffer(S0, _, _). zahl(S0, S1, Ergebnis) :zahl(S0, S1, 0, Ergebnis). r([82|S0], S0). doppelpkt([58|S0], S0). gleichEins([61,49|S0],S0). gleich([61|S0],S0). operator([43|S0], S0, plus). operator([42|S0], S0, mal). operator([45|S0], S0, minus). goto([71, 79, 84, 79, 32 | S0], S0). if([73, 70, 32 | S0], S0). gleichNull([61, 48, 32 | S0], S0). stop([83, 84, 79, 80 | S0], S0). zeilenEnde([10|S0],S0). parse(Code, Programm) :programm(Code, [], Programm).

122

programm(S0, S2, [Befehl|Rest]) :programmZeile(S0, S1, Befehl), programm(S1, S2, Rest). programm(S0, S0, []). programmZeile(S0, SX, Befehl) :zahl(S0, S1, _), doppelpkt(S1, S2), befehl(S2, S3, Befehl), zeilenEnde(S3, SX). befehl(S0, SX, macheEins(I)) :r(S0, S1), zahl(S1, S2, I), gleichEins(S2, SX). befehl(S0, SX, zuweis(Operator, I, J, K)) :r(S0, S1), zahl(S1, S2, I), gleich(S2, S3), r(S3, S4), zahl(S4, S5, J), operator(S5, S6, Operator), r(S6, S7), zahl(S7, SX, K). befehl(S0, SX, goto(L)) :goto(S0, S1), zahl(S1, SX, L). befehl(S0, SX, ifZeroGoto(I, L)) :if(S0, S1), r(S1, S2), zahl(S2, S3, I), gleichNull(S3, S4), goto(S4, S5), zahl(S5, SX, L). befehl(S0, SX, stop) :stop(S0, SX). beispielProgramm( "0:R3=R0+R1 1:R3=R3*R2 2:R2=R2-R1 3:if R2=0 goto 5 4:goto 1 5:stop

123

").

%%%%%%%%%%%%%%%%%%%%%%%%%% interpreter %%%%%%%%%%%%%%%

nthInList([X|_],0,X). nthInList([_|Xs],N,Z):N > 0, M is N - 1, nthInList(Xs,M,Z). nthInList([],N,0):N >= 0. putInList([],0,X,[X]). putInList([],N,X,[0|Xs]):N > 0, M is N - 1, putInList([],M,X,Xs). putInList([_|Xs],0,Y,[Y|Xs]). putInList([X|Xs],N,Y,[X|Ys]):N > 0, M is N - 1, putInList(Xs,M,Y,Ys). varMinus(X,Y,Z):X >= Y, Z is X - Y. varMinus(X,Y,Z):X < Y, Z is 0. operiere(plus, X, Y, Z) :X is Y + Z. operiere(mal, X, Y, Z) :X is Y * Z. operiere(minus, X, Y, Z) :varMinus(Y, Z, X).

ausfuehren(L, Reg, Programm) :nthInList(Programm, L, Befehl), write(’line = ’), write(L), write(’\treg = ’), write(Reg), write(’\t’), write(Befehl), nl, ausfuehren(L, Befehl, Reg, Programm). ausfuehren(L, macheEins(I), Reg, Programm) :putInList(Reg, I, 1, RegNeu),

124

LNeu is L + 1, ausfuehren(LNeu, RegNeu, Programm). ausfuehren(L, zuweis(Operator, I, J, K), Reg, Programm) :nthInList(Reg, J, RJ), nthInList(Reg, K, RK), operiere(Operator, RI, RJ, RK), putInList(Reg, I, RI, RegNeu), LNeu is L + 1, ausfuehren(LNeu, RegNeu, Programm). ausfuehren(_, goto(LNeu), Reg, Programm) :ausfuehren(LNeu, Reg, Programm). ausfuehren(_, ifZeroGoto(I, LNeu), Reg, Programm) :nthInList(Reg, I, RI), RI =:= 0, ausfuehren(LNeu, Reg, Programm). ausfuehren(L, ifZeroGoto(I, _), Reg, Programm) :nthInList(Reg, I, RI), RI =\= 0, LNeu is L + 1, ausfuehren(LNeu, Reg, Programm). ausfuehren(_, stop, Reg, _) :write(’stopped’), nl, write(Reg), nl. ausfuehren(L, 0, _, _) :write(’syntax error in register programm’), nl, write(’at line ’), write(L), nl.

beispiel(0, [4], [ macheEins(1), zuweis(plus, 2, 1, 2), zuweis(mal, 2, 0, 2), zuweis(minus,0, 0, 1), ifZeroGoto(0, 6), goto(2), stop ]).

125