Einen schönen guten Tag allerseits, wie Sie vielleicht wissen,
findet am 30. November der jährliche, die es Akademikus der Universität statt.
Eine Veranstaltung, die primär dazu dient,
sich über universitäres Lehren lernen und forschen zu verständigen.
Es gibt da eine Reihe von zentralen Veranstaltungen,
um Ihnen die Gelegenheit zu geben,
an diesen Veranstaltungen teilzunehmen,
bittet der Präsident darum,
dass Ihnen
keine Nachteile daraus erwachsen
Sollen. Wir haben uns deshalb überlegt, wie wir damit umgehen,
weil der 30. ein Mittwoch ist und da sind sechs von unseren neuen Übungsgruppen.
Und die Lösung, die uns eingefallen ist, sieht so aus.
Von den sechs Übungsgruppen, die wir haben, wollen wir versuchen.
Das heißt, es ist jetzt ein Angebot,
was in der nächsten Woche in den Übungsgruppen diskutiert werden muss,
wollen wir versuchen, vier nach wie vor am Mittwoch anzubieten,
sodass also alle vier Slots abgedeckt sind,
acht bis zehn,
zehn bis zwölf
12 bis 14 und 14 bis 16 Uhr. Allerdings dort,
wo doppelt Angebote sind, dass wir die Gruppen auf den Dienstag bzw. Donnerstag
um 16 Uhr verschieben. Dadurch hätten sie die Möglichkeit,
wenn sie die Übungsgruppen am Mittwoch wahrnehmen wollen,
dies zu tun, so wie das bis sonst immer ist,
wenn sie stattdessen lieber die Veranstaltung des Akademikus besuchen wollen,
dann gibt es eine Ausweichmöglichkeit.
Selbstverständlich stehen auch die Veranstaltungen, Dienstag von 8 bis 10
und Donnerstag 8 bis 10 und 10 bis 12
als Ausweichmöglichkeit
zur Verfügung
Und ich hoffe, damit haben wir eine Regelung, die allen einigermaßen entgegenkommt.
Wir werden also in den Übungsgruppen am Mittwoch dann,
also nächste Woche Mittwoch, versuchen einen Überblick zu bekommen,
wie groß die Wechselwilligkeit für die betreffende Woche tatsächlich ist.
Und je nachdem dann darauf reagieren,
die Räume für diese Ersatzgruppen liegen noch nicht fest,
die würden wir dann je nach Bedarf vorsehen.
Eine Notlösung haben wir auf jeden Fall und irgendwo finden kommen wir unter.
Das würden wir dann über Stine bekannt geben.
So viel dazu, das ist die Frage des DSAkademikus
Wir hatten uns angefangen mit rekursiven Datenstrukturen zu beschäftigen und hatten
als einen einfachen Fall einer rekursiven Datenstruktur PANO-Zahlen eingeführt.
Und da wir immer interessiert dran sind, ein möglichst anschaulichen
Unterricht zu bieten, habe ich ihn hier anschauungsmaterial mitgebracht.
Dies ist ein Wass. Könnte man meinen, ja, für mich ist das eine PANO-Zahl. Sie sehen die Ähnlichkeit zwischen einer Struktur einer PANO-Zahl und einer Matroschka,
wenn ich feststellen will, ob das eine PA-No-Zahl ist,
muss ich gucken, ob es das Entelement,
die Null ist, das Null-Element, ist es nicht,
denn ich kann es,
ich kann eine S-Schale außen drumherum abmachen und innen drin kommt wieder ein Objekt,
was aussieht wie eine S-Schale, das ist kein Null-Element,
ich kann es öffnen, da drin kommt wieder eine,
Matroschka zum Vorschein,
ich kann das wieder öffnen,
indem ich S-Schale für S-Schale außen ab montiere und immer drin gucke,
ist das
ein Null-Element oder ist es das nicht
Und werde ich irgendwo ganz im Inneren angekommen bin.
Heureka, da kommt ein Null-Element zum Vorschein. Ich kann
genauso umgekehrt eine PA-Nutzer konstruieren, indem ich ein Null-Element nehme,
außen drumherum eine Essschale setze und das Rekusiv weitermache,
indem ich Essschale für Essschale hinzufüge,
sodass zum Schluss wieder eine PA-Nutzahl herauskommt.
Woher weiß ich, welchen Wert
diese PA-Nutzahl hat
Ich muss zählen, wie viele Schichten ich abmontiert habe,
beziehungsweise umgekehrt, wie viele Schichten ich drauf gesetzt habe.
Das ist alles, was man machen muss.
Es ist auch die einzige Möglichkeit, dies festzustellen.
Von außen kann ich nicht feststellen,
was der arithmetische Wert einer solchen PA-Notzahl ist.
Wenn ich das ermitteln will, muss ich das Ganze rekursiv zerlegen.
Das können wir hier mit dem Programm noch nicht machen.
Das Programm kann eins und allein feststellen,
ist es eine PA-Notzahl oder ist es keine PA-Notzahl?
Mehr ist ja im Augenblick
noch nicht möglich
Wir hatten uns angeguckt, dass wir das also tatsächlich
dazu verwenden können, einen Test zu machen.
Wir instanzieren das Argument und kriegen dann einen Wahrheitswert heraus,
der uns sagt, ist es eine PA-Notzahl, ja oder nein?
Im Übrigen ist also auch klar,
wenn hier innen drin nicht irgendwie das vorgesehene Nullelement steckt,
sondern eine Zigarettenkippe oder eine Murmel oder sonst irgendetwas,
dann ist das kein wohlgeformmte PA-Notzahl oder keine wohlgeformte Matroschka,
dann hat sie der Verkäufer über den Tisch gezogen.
Und genauso ist es hier, ich muss es tatsächlich zerlegen
bis zum letzten Element und erst dann bis zum Abschlusselement oder dem Null-Element.
Und dann kann ich feststellen, ob es wirklich eine wohlgeformte
PA-Notzahl ist oder nicht. Ich kann das Ganze uninstanziert aufrufen,
indem ich eine Valabel als Argument verwende und dann,
dann fängt das Programm an,
PA-Notzahlen zu generieren,
beginnend beim Null-Element PA eine Essschale nach der anderen wird draußen drumherum gebaut,
so wie wir das
eben bei dem Matroschka
auch hatten
Gut, wir haben hier jetzt einen Fall von Rekursion vorliegen,
der sich von dem Fall von Rekursion,
den wir vorher hatten, unterscheidet, dadurch,
dass wir hier zwar wieder eine selbstbezügliche Definition eines Prädikats hatten,
genauso wie wir das bei Zwilling von hatten,
genauso wie wir das bei transitiver Erreichbarkeit oder Teil von Beziehung beispielsweise hatten.
Und der zentrale Unterschied ist jetzt hier,
dass die Rekursion nicht mehr unbeschränkt erfolgt,
sondern dass sie über einer Datenstruktur,
über einer rekursiven Datenstruktur passiert,
die entweder rekursiv zerlegt wird oder
rekursiv aufgebaut wird
Und da ich in diesem Falle,
wenn ich ein komplexes Ding wie diese Matroschka nehme und Schritt für Schritt
zerlege und eine komplexe Matroschka in eine immer einfachere Matroschka verwandle,
kann ich garantieren, dass ich irgendwann mal beim Abschlusselement vorbeikommen muss,
bei der Null vorbeikommen muss.
Und wenn ich eine solche Terminierungsbedingung habe, dann kann ich
in diesem Fall auch Terminierung des gesamten Programms garantieren.
Das ist eben der Unterschied einer deduktiven Datenbank,
wo wir nicht ohne weiteres davon ausgehen konnten,
weil wir ja keine Datenstruktur haben, die wir konsumieren können.
Wir konnten nicht davon ausgehen, ob das Ganze,
ob die Datenbank nicht Zyklen enthält
Und wenn sie Zyklen enthält, dann können wir die Terminierung
eben nicht garantieren. Aber selbst wenn sie keine Zyklen enthält,
haben wir gesehen, dass wir im Falle,
also wenn wir nicht die Pattern mit der Instanzierung auf der Datenbank verwenden,
dass wir dann in Terminierungsprobleme geraten, sodass wir immer dafür Sorge tragen müssen,
dass wir die Anfrage möglichst viel mit Informationen anreichern,
Informationen aus der Datenbasis, damit die Terminierung garantiert ist.
Gut,
also diese Konsuktion der Datenstruktur ist hier dargestellt in dem Bild,
was ein komplexes Problem immer wieder auf ein einfaches Problem zurückführt,
bis ich irgendwann mal eine Terminierungsbedingung vorfinde,
die das Ganze zum Halten bringt
Ein wichtiger Spezialfall, davon ist die sogenannte Endrekussion und Endrekussion,
heißt, dass Berechnungen immer nur beim rekursiven Abstieg passieren.
In dem Augenblick, wo ich, wo eine Prozedur
sich selbst aufruft, dann passieren die entscheidenden Berechnungen.
Es gibt Programme, die das nicht erfüllen können,
die einen allgemeineren Fall von Rekursionen darstellen,
die gucken wir uns dann später an.
Wir fangen mit dem einfachsten an,
das ist die Endrekursion, jetzt passiert also Berechnung immer nur
eben zum Zeitpunkt des Aufrufs einer Prozedur
Und nicht zum Zeitpunkt der Rückgabe ihrer Berechnungsergebnisse.
Und weil bei der Rückgabe keine Berechnung mehr stattfinden,
ist also das Ergebnis beim Rekursionsabschluss bereits vollständig bekannt.
Ich muss keine Zwischenergebnisse aufbewahren, um sich später noch weiterverwenden zu können.
Und in diesem Falle ist ein Verarbeitungssystem immer in der Lage,
ein solches rekursives Programm in einen dann auch effizienteren,
iterativen Code zu übersetzen.
Die Effizienz kommt einfach daher, weil ich den Stack nicht brauche.
Ich muss die ganze Stack-Verwaltung nicht mitschleppen und deshalb ist
das Ganze auf einer im Wesentlichen iterativ arbeitenden Maschine viel einfacher
zu implementieren
Und formal kann man also Endrikusion immer dadurch erscheinen,
dadurch erkennen, das kennen sie vielleicht aus der funktionalen Programmierung auch,
wenn der rekusive Aufruf der letzte Funktionsaufruf ist,
so heißt es in der Funktion, in der Almprogrammierung.
Oder hier heißt es, wenn es das letzte Teilziel
im Rekusionsschritt ist, dann ist das ganze Endrikusie.
Man kann es also sehr einfach erkennen,
wenn wir uns uns unser Programm angucken hier,
dann haben wir nur den Rekusionsschritt, der ist relativ trivial.
Und in diesem Falle gibt es nur ein Teilziel im Körper der Klausel und damit ist,
dass das letzte Teilziel, was aktiviert wird,
das Ganze ist endrikusiv und das sehen wir tatsächlich auch in dem Berechnungsprozessen
Die Ergebnisse sind immer bekannt, wir gucken uns uns hier an.
Die Ergebnisse sind immer bereits bekannt, wenn ich hier unten an
den Blattknoten ankomme. Das ist vielleicht besser hier zu erkennen.
Hier instanziere ich die Variable X1 und in dem Augenblick,
wo ich dieses Teil, diese Klausel hier zur Berechnung heranziehe,
kenne ich das Gesamtergebnis. Dann weiß ich nämlich,
dass X gleich S von X1 ist und das X1 gleich Null ist,
sodass ich also in diesem Zeit,
zu diesem Zeitpunkt bereits weiß,
das X hat den Wert S von Null
Das hatte ich das letzte Mal damit verglichen mit einem guten Zauberer,
der also auch die tatsächlichen Berechnungen immer zu einem Zeitpunkt oder der Trick,
den Trick immer dann dadurch vorführt, oder nein,
dass der Trick immer dann passiert,
wenn der Zuschauer noch gar nicht damit rechnet.
Und in dem Augenblick, wo der Trick vorgeführt wird,
wo das Ergebnis gezeigt wird, dann ist alles schon längst passiert.
Also der Betrug passiert nicht in dem Augenblick,
wo die ganze Sache offensichtlich wird, sondern weitaus her.
Und das ist hier genauso das Teilergebnis steht hier bereits fest.
Zu diesem Zeitpunkt weiß ich bereits,
dass das Ergebnis mal so aussehen wird
Alles, was ich nicht kenne, ist das Innere und das
weiß ich in diesem Augenblick und dann kann ich plötzlich zeigen,
hallo, ich kenne Ehren, das Gesamtergebnis bereits.
Weil die entscheidenden Berechnungen hier beim Abstieg passiert sind. Und dieses Zwischenergebnis,
das, was hier für das eine Ergebnis verwendet wird,
wird unverändert, auch für das zweite Ergebnis benutzt.
Indem ich jetzt sage, okay, gibt es noch Alternative,
Variablenbindungen für dieses X1 hier? Ja, natürlich gibt es die,
denn ich habe eine Rekursionsschritt noch zur Verfügung,
der es mir erlaubt, X1 als S von etwas zu ermitteln.
Nur, dass ich dieses Etwas, das X2 noch nicht kenne,
was ich allerdings mit dem Rekursionsabschluss, mit dem Fakt in der Definition erledigen kann.
In dem Augenblick kenne ich den Wert von X2 gleich 0 und ich weiß,
dass der Gesamtwert X von S von X1,
X1 ist S von X2, X2 ist 0,
also S von S von 0 ist.
Und dieses Teilergebnis, das ist S von S von irgendetwas ist, das stand hier längst fest.
Die Berechnung ist längst passiert in dem Augenblick, wo das Ergebnis hier präsentiert wird. Gut,
Ja, also ein wichtiger Fall von,
der wichtige Spezialfall von der Endrekursion liegt hier vor,
wir werden also im Weiteren immer prüfen müssen, ob unsere Definitionen,
die wir aufbauen, diese Eigenschaft der Endrikursion haben,
wenn sie die nicht haben, dann werden wir überlegen müssen,
ob wir jetzt eine Möglichkeit gibt,
eine endrikusive Definition zu kreieren.
Das wird in vielen Fällen möglich sein,
aber es gibt Fälle, wo es nicht so ohne weiteres
möglich ist und wir müssen den Unterschied erkennen.
Wann ist es möglich,
eine Endrikursive Definition anzugeben,
wann ist es nicht möglich
Gut, jetzt machen wir also weiter.
Wir haben das, wir wollen ja irgendetwas berechnen.
Wir haben bisher nur einen Typtest gemacht. Jetzt gehen wir
mal den nächsten Schritt. Wir wollen tatsächlich etwas berechnen.
Wir haben eine zweite Matroschka. Und wir wollen wissen,
welche von beiden größer oder kleiner ist.
Wie können wir es machen? Können wir es von außen
feststellen? Ja, wie dann? Wie durch.
Immer ein Abnehmen vorbei und den nächsten stoppen wir dafür gleich
Genau, das rekursiv immer fortsetzen, indem wir eine Essschale auf Biden abnehmen.
Was ist, kennen Sie eine gute Metapher,
um solchen Größenvergleich mit einem einfachen Alltagsgegenstand sich zu veranschaulichen?
Also wo man diesen Prozess oder womit würde man hier,
es ist ja das kleine Alz, was hier zur Debatte steht,
gibt es irgendetwas, wie man für diskrete Elemente,
wir reden ja über natürliche Zahlen hier,
für diskrete Elemente,
sich diesen Größenvergleich zwischen Mengenangaben veranschaulichen kann
Hat dir mal eine Idee, was man da ist,
gute Metapher einsetzen kann? Das ist die Waage.
Das ist ganz einfach. Sie haben eine Waage und
die soll im Ungleichgewicht sein. Sie haben eine Schale,
auf der ein paar Murmeln liegen und sie haben eine andere Schale,
auf der weniger Murmeln liegen.
Und dann senkt sich die Waage hier nach unten.
Jetzt stellen Sie sich vor, da liegen nicht drei Murmeln,
sondern die 508 Murmeln
Wie kann man, oder ja, eine Porzedur,
die genau diesem auseinander neben der Matroschka entspricht, würde jetzt heißen,
ich nehme von jeder Schale eine Murmel weg und an dem Gleichgewicht ändert sich nichts.
Dass der, also an dem Ungleichgewicht, sorry,
an dem Ungleichgewicht ändert sich nichts,
auch wenn ich eine weitere Murmel wegnehme,
ändert sich an dem Ungleichgewicht nichts.
Und in dem Augenblick bin ich hier bei null Elementen angekommen
und hier sind noch Elemente verblieben und dann weiß ich eben,
dass dies hier die größere im Vergleich zu dieser natürlichen Zahl
gewesen ist
So, und nichts anderes machen wir hier. Wir können also sagen,
wenn wir hier ein Null-Element haben und hier irgendeine PANO-Zahl,
die außenrum nur S von irgendwas heißt,
dann ist das erste kleiner als das zweite.
Und wenn dieser Fall nicht passt, wenn der nicht vorliegt,
dass hier vorn eine Null gefordert wird,
sondern wir haben hier eine PA-Notzahl und hier eine PA-Notzahl,
dann nehmen wir eben von beiden eine Essschale außen weg und vergleichen,
ob die immer noch in der Beziehung kleiner als stehen.
Also diese Regel hier ist nichts anderes als dieses Wegnehmen von Kugeln auf der Waage.
Wir behalten das Gleichgewicht bei an der An der Eigenschaft des
ersten Arguments kleiner zu sein als das zweite Argument,
ändert sich durch das Wegnehmen der Kugeln überhaupt nichts.
Und genau das machen wir hier. Hier verschwindet eine Kugel,
hier verschwindet eine Kugel. Und das sind die restlichen Kugeln
, die übrig bleiben. Wir machen jetzt einen Schritt,
den wir im Weiteren des Öfteren noch machen werden,
dass wir versuchen, wegzukommen von einer prozeduralen Denkweise.
Wir machen ja deklarative Programmierung, deklarative Programmierung heißt sagen,
wie es ist. Wir wollen nur spezifizieren,
wie die Welt aussieht
Wir haben hier durch eine einfache Metapher, mit der Matroschka und der Waage,
uns abgeleitet, dass das eine vernünftige Prädikatsdefinition ist.
Aber wie können wir die so eine Prädikatsdefinition in anderen Fällen auch ableiten,
wo wir so eine Metapher nicht zur Verfügung haben?
Eine Möglichkeit besteht darin, wir machen ja deklarative Programmierung.
Wir schreiben uns erstmal Testfälle auf. Wir haben positive Testfälle und negative Testfälle.
Also wir wollen die Null so kleiner als die Eins sein.
Das Resultat soll Truh sein. Die Null soll auch kleiner
als die Zwei sein, ebenfalls erwartetes Resultates Truh.
Die Eins soll kleiner als die Zwei sein. Und dann
haben wir negative Aufrufe, die Null soll nicht kleiner als die Null sein.
Die eins soll nicht kleiner als die Null sein,
die zwei sollen nicht kleiner als die eins sein und so weiter.
Davon können wir natürlich Beliebier aufschreiben. Die Kunst besteht immer darin,
die relevanten Fälle hiermit vorzusehen. Das Ganze ist ja rekursiv.
Das heißt, irgendwann können wir dann mal abbrechen,
weil wir eben eine Beziehung haben, wenn es für das kleinere gilt,
dann muss es für das nächstgrößere auch gelten.
Und jetzt können wir hingehen und können auf die positiven Resultate gucken.
Die negativen brauchen wir uns gar nicht anzugucken. Die dienen
nur hinterher zum Nachweis, dass das negative Resultat auch wirklich rauskommt.
Wir schauen jetzt hier auf diese Beispielfälle und versuchen herauszufinden,
was haben die Dinger gemeinsam?
Oder wo gibt es gemeinsame Fälle? Und diese gemeinsamen Fälle,
internalisieren wir in Form einer Regel oder in Form einer Klausel allgemeiner gesagt.
Und die erste Gemeinsamkeit, die WM entdecken ist,
hier sind zwei Fälle, die beide eine Null hier davor haben.
Und wir könnten das erweitern, indem wir den Fall nehmen,
die Null soll auch kleiner als die drei sein soll,
auch kleiner als die vier sein.
Dieses Pattern ändert sich dadurch nicht. Die Null ist immer kleiner
als irgendetwas mit einem, was mit einem S beginnt.
Und egal, was hier drin ist, Hauptsache außen drum
ist eine Essschale, dann soll das Ganze wahr sein.
Diese beiden Klauseln, diese beiden Testfälle zusammengenommen,
führen uns auf die erste Klausel.
Wann immer hier vorne eine Null steht,
muss hier hinten
ein Essschalenausdruck stehen
Und das Nächste ist, dass wir jetzt durch die Brille
der Unifikation auf unsere Argumentbelegungen schauen und hin versuchen rauszukriegen,
wie wir irgendwie durch Unifizieren der Therme, die wir dort haben,
einen Fall auf einen anderen zurückführen können.
Und da sehen wir, dass wir diesen Fall hier,
der ja noch übrig geblieben ist,
durch Unifikation mit einem S von X,
an beiden Seiten, MS von X,
MS von Y,
auf diesem Fall
hier zurückführen können
Und genau das schreiben wir hier hin.
Wir unifizieren das mit einem S von X-Ausdruck. Das steht
hier. Wir unifizieren das mit einem S von Y-Ausdruck.
Und dann wird es auf den gleichen Größenvergleich zurückgeführt.
Für Größe von X von Y. Also die Kunst durch generalisieren
aus Beispielen, Programme abzuleiten. Und nichts anderes ist ja,
relationale Abstraktion, dass ich vom Beispiel wegkomme,
hin zu einer generellen Beschreibung für alle Fälle,
die ich vorliegen habe
Genauso wie funktionale Abstraktion eben die Kunst ist,
von einer funktionalen Wertetabelle hin zu einem Funktionsausdruck zu kommen,
der genau die hätte dieser Tabelle berechnet.
Genau, das machen wir hier eben nur mit Relation.
Und da können wir solche Beispiele als Ausgangspunkt nehmen und versuchen,
Gemeinsamkeiten zu finden, die uns möglichst viele dieser Testfälle mit wenigem Aufwand erschlagen.
Da hören wir eben diese beiden Fakten zusammenführen und
diesen Fall hier plus alle, die noch weiteren Fälle,
die wir haben,
können wir dann auf diese Regel
hier unten abbilden
So, jetzt testen wir das Ganze. Wir fangen an,
natürlich mit vollständig spezifizierten Aufrufen, die überspringe ich jetzt hier einmal,
weil wir dann rekursive Datenstrukturen auf beiden Argumentpositionen haben.
Und das in der Definition praktisch schon drinsteht, wir konsumieren diese,
das heißt Terminierung ist garantiert und wir enden immer dann,
wenn auf dem kleineren, auf dem kleineren Argument,
auf dem ersten Argument die Null erreicht ist.
Wenn das sich nicht machen lässt,
dann gilt die Relation kleiner als offensichtlich nicht
Die größere Zahl auf dem ersten Argument ist,
dann soll ja False herauskommen. Dann führt es dazu,
dass ich rekursiv abbaue und irgendwann mal damit ende,
dass hier eine Null erscheint und hier immer noch eine Essschale da ist.
Und dann gibt es keine einzige Klausel, die für diesen Fall noch zuschlägt.
Also wenn ich das mit diesem Beispiel hier aufrufe,
dann ist weder der Rekursionsabschluss noch der Rekursion Schritt einschlägig.
Beide unifizieren nicht. Deshalb gibt es kein Ergebnis und es kommt
wie gewünscht False heraus. Gut, also jetzt der Trace,
wir rufen das Ganze teilweise spezifiziert auf das erste Argument ist unspezifiziert.
Das zweite ist die 2. Wir haben einen Entscheidungspunkt mit zwei Klauseln.
Mehr hat ja unsere Definition nicht. Wir unifizieren mit der ersten Klausel und stellen fest,
dass das Ganze erfolgreich ist unter der Bindung X gleich 0.
Und damit kriegen wir als erstes Ergebnis ausgegeben,
jawohl eine Zahl, die kleiner ist als zwei, ist die 0.
Wenn wir nach Alternativen fragen, kommen wir zum Rekursionsschritt,
der Rekursionsschritt, Unifiziert ebenfalls mit der Variablenbindung X,
gleich S von X1 und dem der Variablenbindung Y1 ist gleich das Innere dieses Ausdrucks hier,
also S von Null.
Das sind diese beiden Variablenbindungen, die wir erzeugen.
Damit machen wir jetzt einen neuen Entscheidungspunkt auf.
Mit diesem Variablenbindung müssen wir jetzt den Körper der Klausel aufrufen,
also ein neuer Entscheidungspunkt,
wieder zwei Klauseln zur Auswahl der Rekursionsabschluss und der Rekursionsschritt,
der Rekursionsabschluss unifiziert mit der Variablenbindung X gleich 1,
X1,
sorry,
X1 gleich Null
Hier hinten keine weiteren Informationen, das unifiziert eben einfach,
das ist gelingt, aber da wird ja keine Variable gebunden, denn das ist die anonyme Variable.
Das heißt, wir haben eine Variablenbindung für X1 jetzt,
und da wir wissen, dass X1 das Innere dieses S-Ausdrucks ist,
der SX ausmacht, hat X den Wert S von Null.
Und das kriegen wir als zweites Ergebnis ausgegeben. Das heißt,
das zweite Ergebnis ist, eine Zahl,
die kleiner ist als 2 ist,
beispielsweise die 1. Und wenn wir wieder nach Alternativen fragen,
dann kommen wir wieder zu der Regel,
Der Regelkopf unifiziert tatsächlich mit unserer Anfrage,
das war ja die Anfrage hier, mit der Variablenbindung X1,
ist gleich S von X2.
Und der Variablenbindung Y2 ist gleich null.
Das sind die beiden Variablenbindungen, die hier hinten stehen.
Und mit diesen Variablenbindungen muss ich jetzt den Körper der Klausel aufrufen.
X2 ist nach wie vor uninstanziert. Ich kenne keinen Wert
dafür. Y2 ist gleich null. Und jetzt kriege ich
diesen Fall hier, dass ich nachweisen muss,
dass X2 kleiner ist
als null
Der Fakt unifiziert nicht, weil der hier eine S-Ausdruck erwartet.
Hier ist aber eine Zahl. Und die Regel unifiziert nicht,
weil sie auf beiden Argumentpositionen einen S-Ausdruck erwartet.
Hier ist aber ein die Zahl. Das heißt,
weder der Fakt noch die Regel unifizieren, beide scheitern.
Damit habe ich alle Klauseln im zweiten Entscheidungspunkt abgearbeitet.
Ich kam Backtracking zum ersten Entscheidungspunkt machen.
Da hatte ich allerdings auch bereits beide Klauseln abgearbeitet.
Das heißt, weitere Ergebnisse gibt es nicht.
Das sind die einzelnen beiden Bindungen, die ich bekomme dafür.
Und das ist ja auch das,
was wir erwarten von den natürlichen Zahlen ist eben,
sowohl die Null als auch die eins kleiner als zwei
Wir können jetzt andersherum aufrufen, indem wir sagen,
ja, was, welche Zahlen sind denn kleiner als,
nein, welche Zahlen stehen in der Relation zu zwei,
sodass zwei kleiner ist als diese Zahlen.
Also mit anderen Worten, alle Zahlen, die größer sind als zwei,
weil wir als Ergebnis haben. Wir haben diesen Aufruf hier,
wir unifizieren mit dem Regulsionsabschluss, das gelingt nicht,
weil hier ein S-Ausdruck ist, hier ist eine Zahl.
Das heißt, wir kommen über das Backtracking zum zweiten,
zur zweiten Klausel, das ist unsere Regel, unser Rekursionsschritt.
Auf der ersten Argumentstelle ist es erfolgreich,
X1 wird ein SV 0 gebunden, das ist hier aufgeschrieben.
Y1 wird an, Quatsch, X wird an S von Y1 gebunden.
Das ist das, was hier hinten steht. Und mit
diesen vaiablen Bindungen wird der Körper der Klausel erneut aufgerufen.
Das heißt, ich habe jetzt einen Aufruf,
S von 0 sei kleiner als Y1 sein
Ich nehme wieder den Rekussionsschritt. Das scheitert aus genau dem gleichen Grund,
weil hier ein komplexer Term steht und hier ein Atom,
das unifiziert nicht. Ich komme zur Rekussion Schritt,
der Rekussion Schritt unifiziert mit der vaiablen Bindung X2 gleich null
und der Variablenbindung Y1 gleich S von Y2.
Da sind diese beiden Variablenbindungen, die ich hier hinten habe,
mit denen rufe ich jetzt wieder den Körper der Klausel auf und das heißt,
ist die null kleiner als das Innere dieses Ausdrucks hier.
Ich komme wieder zum, also dass der Aufruf,
das Y2 ist uninstanziert. Den Wert kenne ich noch nicht.
Dieses Argument ist instanziert. Das heißt, wir haben,
wenn man das vergleicht, eine ähnliche Situation in allen Ebenen.
Erstes Argument ist immer instanziert. Zweites Argument ist nicht instanziert
. Erstes Argument instanziert, zweites nicht. Auch hier erstes
Argument instanziert. Zweites ist nicht instanziert. Alles,
was wir sehen, ist, dass hier eine Konsumption stattfindet.
Von der zwei vereinfachen wir das Ganze auf die Eins
und jetzt haben wir es auf die Null vereinfacht.
Jetzt unifiziert endlich unser Rekursionsabschluss. Die Null punifiziert mit
der Null und Y2 wird ein S von irgendetwas gebunden
Ja, das ist ja genau das, was in unserem Regulsionsabschluss drinsteht.
Das heißt, wir wissen jetzt den Wert von Y 2
, das ist S von irgendetwas. Und das kriegen wir
als Ergebnis ausgegeben. Wieder nach dem gleichen Trick,
wir kannten eigentlich schon alles, wir wussten,
dass X gleich S von Y 1 ist.
Wir wussten hier, dass X gleich S von S von Y 2 ist.
Und jetzt kennen wir auch Y 2. Und Y,
dadurch ist X bekannt als S von S von S
von irgendetwas. Und das kriegen wir als Ergebnis ausgegeben
Was wir hier sehen, ist ein sogenanntes, unterspezifiziertes Resultat.
Was wir bisher hatten, sind immer nur unterspezifizierte Argumentbelegungen gewesen.
Dass wir vajablen, haltige Ausdrücke im Input hatten.
Jetzt haben wir plötzlich vajablenhaltige Werte im Output.
Wir kriegen einen Ausdruck, der generisch unendlich viele S-Strukturen beschreibt.
Und das ist genau das, was wir haben wollen.
Denn natürliche Zahlen größer zwei, davon gibt es nun mal
unendlich viele. Und alle sind durch diesen Ausdruck beschrieben,
wenn sie damit unifizieren, sind es korrekte Ergebnisse.
Das heißt, ich bin in der Lage,
unendlich viele Ergebnisse mit einem einzigen Ausdruck
zu beschreiben
Das ist der zweite Blick, den wir in die Tiefen
der Unendlichkeit tun. Wir sind jetzt in der Lage,
mit einem endlichen Ausdruck über unendlich viele Berechnungsergebnisse zu reden.
Auch das ist ein Alleinstellungsmerkmal der Logikprogrammierung. Das werden sie
nirgendwo anders finden. Wir können natürlich wieder noch
nach Alternativen fragen und kommen dann wieder zu der Regel.
Den Fakt hatten wir ja bereits überprüft. Jetzt müssen wir
die Regel überprüfen. Da sehen wir aber wieder,
das scheitert, weil hier ein Atom ist, sorry hier,
ein Atom ist und hier ein komplexer Therm steht.
Deshalb unifiziert der Kopf der Klausel nicht. Und wir haben
den dritten Entscheidungspunkt vollständig abgearbeitet, Backtracking zum Zweiten bringt nichts Neues,
weil wir bereits beide Klauseln verwendet haben,
das Gleiche im Entscheidungspunkt 1. Das heißt, weitere Ergebnisse gibt es nicht.
Und in der Tat haben wir alle Ergebnisse,
die möglich sind, hiermit ja bereits ausgedrückt.
Es gibt kein einziges weiteres Ergebnis. Alle sind hier bereits subsummiert.
Und wir sehen damit sozusagen einen zweiten Fall oder wieder einen Fall
davon, dass Berechnung in der Logikprogrammierung immer Informationsanreicherung ist.
Wir gehen hier mit einem Unterspiel,
mit hier mit einem unterspezifizierten Ausdruck in die Berechnung hinein und
wir kriegen das X so weit instanziert oder mit Informationen angereichert,
wie es das Programm ermöglicht
Und das Programm ermöglicht es eben genau zu sagen, ja,
das X wird ein S von S von irgendetwas sein.
Dieses irgendetwas kann ich leider nicht instanzieren, die Information fehlt mir.
Mehr Informationen habe ich nicht zur Verfügung. Und das ist
genau das, was einen größten Vergleich auch leisten muss.
Was wir auch sehen, ist hier,
dass wir jetzt einen relationalen Verwendungsweise einer Größen,
einer Ordnungsrelation ermöglicht haben,
wo im klassischen Fall immer nur die Testvariante möglich war
Also Größenvergleich in jeder anderen Programmiersprache heißt immer,
ich gebe Werte hinein und ich kriege einen Bullshen-Wert heraus.
Hier geben wir jetzt sozusagen einen unterspezitierten Aufruf hinein und
wir kriegen alle Berechnungsergebnisse, die dieser Relation genügen.
Und das sind eben manchmal unendlich viele, im Falle kleiner,
als waren es endlich viele. Jetzt sind es unendlich viele.
Gut, das war ein Fall von direkter Rekursion.
Wir machen jetzt einen Fall von indirekter Rekursion.
Könnte ich Sie bitten, Ihr Geschäft draußen zu erledigen?
Wäre das irgendwie möglich
Einen Fall von indirekter Rekursion, indirekter Rekursion heißt,
dass ein Prädikat sich nicht selbst aufruft, sondern ein zweites Prädikat aufruft.
Und dieses zweite, das erste Wiederaufruf,
wodurch ein zyklischer Aufruf zwischen zwei unterschiedlichen Prädikaten entsteht.
Der Zyklus muss nicht die Länge 2 haben,
das können beliebig viele Prädikate sein, die dort dazwischen liegen.
Wir gucken uns natürlich den einfachsten Fall an und das sind dann zwei Prädikate beteiligt.
Dieser einfache Fall heißt, ich möchte definieren,
was eine ungerade und was eine gerade Zahl ist.
Natürlich PA-Notzahlen
Man kann das auch beliebig andere Art und Weise definieren.
Das ist eine Möglichkeit, ein solches Prädikat aufzuschreiben.
Es geht natürlich auch anders. Ich kann geradzeitig und ungeradzeitig
auf andere Art und Weise definieren. Aber das ist eben
ein schöner Fall von indirekter Rekursion, den man sich angucken kann.
Und dann hat man eben jetzt für diese beiden miteinander verbundenen Prädikate wieder positive und negative gewünschte Ergebnisse,
also die Null soll geradzeitig sein,
die eins soll ungeratzeitig sein, die zwei soll geradzahlig sein,
die drei ungeratzahlig und so weiter.
Und auf der umgekehrten Seite für Die Null soll nicht ungeratzahlig
sein, die Eins soll ungeratzahlig sein und so weiter.
Und wir wollen ja jetzt immer eine Beziehung haben,
wie wir Even of Ott zurückführen können und wie wir Ott auf Even zurückführen können.
Das ist das Ziel. Das heißt, wenn wir jetzt
auf die positiven Beispiele schauen, müssen wir immer gucken,
wie wir das eine auf das andere zurückführen können.
Und dann sehen wir eben durch die Brille unserer Unifikation.
Wir können Therme aufbauen, wir können Therme durch Unifikation vereinfachen.
Wir können also beispielsweise diesen Therm hier vereinfachen, indem wir
per Unifikation eine Essschale wegnehmen und das Innere hier rüberreichen.
Und dann sehen wir, dass wir diesen Fall auf
diesen Fall zurückgeführt haben
Und wir können das Gleiche hier machen, indem wir eine Essschale wegnehmen.
Und da können wir diesen Fall auf diesen Fall zurückführen.
Oder wir können diesen Fall hier nehmen, ist ja auch ein Truhfall.
Wir nehmen eine Essschale weg und dann haben wir diesen Fall vorliegen.
Und genau das gießen wir in ein Prädikat.
Wir sagen, die Null ist gradzahlig, das steht hier.
Und wir sagen, eine PA-No-Zahl, die geradzahlig ist,
wenn wir ihr eine Essschale wegnehmen,
dann bleibt dem Inneren eine ungeradzahlige PA-Nozahl übrig.
Und das Gleiche umgekehrt, wenn wir auch von einer ungeradzahligen Zahl
eine Essschale wegnehmen, dann bleibt eine geradzahlige PA-Notzahl übrig.
Das ist alles. Wir sehen also, wir können relativ
gut von den Beispielen direkt unmittelbar die Prädikatsdependition ableiten.
Was wir hier haben, ist eben diese indirekte Aufruf,
wo das eine Prädikat, das andere aufruft und so weiter,
bis irgendwann mal der Rekursionsabschluss erreicht ist und den haben wir hier mit der Null definiert.
Ja, bis jetzt haben wir noch immer,
ja das, was klassischerweise Tests sind, geschrieben.
Wir können uns das hier auch angucken. Wir werden jetzt
in der Lage wieder Durch unterschiedliche Aufrufvarianten geratzahlige Ergebnisse zu generieren.
Ungeratzahlige Ergebnisse zu generieren. Wir können testen, ob eine
PA-Notzahl geratzahlig oder ungeratzahlig ist, all diese Instanzierungsvarianten gehen.
Aber wir haben noch nicht eine einzige Rechenoperation gemacht.
Das sind bisher immer Generieren von Beispielen oder testen, ob ein Beispiel vorliegt.
Der erste Fall, wo wir tatsächlich mal etwas berechnen,
ist die Addition von zwei PA-No-Zahlen und damit soll es dann auch genug sein.
Wir machen das ja hier nicht,
weil das eine besonders intelligente Art und Weise ist,
Athmetik zu betreiben, sondern Weil wir zeigen, weil ich zeigen möchte,
wie man mit rekursiven Datenstrukturen rekursive Präzeduren schreiben kann.
Und die einfachste rekursive Datenstruktur, wie die wir haben,
ist die natürliche Zahl. Eine einfachere gibt es nicht.
Deshalb nehme ich die natürlichen Zahlen hier als Beispiel.
Damit man das an dem einfachen Fall sehen kann. Und auch dann,
wenn es komplizierter wird,
wenn wir also nicht mehr nur noch einstellen hier,
Therme haben, sondern mehrstellige Therme haben,
dass man auch dann noch die zugrunde liegende Muster erkennen kann.
Gut, also jetzt das erste Beispiel.
Wir wollen mal addieren. Wir haben zwei PANO-Zahlen gegeben und
wir wollen deren Summe ermitteln
Wir gehen wieder wie gebaut vor. Wir nehmen positive Beispiele,
die null mit der null addiert, soll die Null ergeben.
Die Null mit der Eins addiert, soll die Einser geben,
die Eins mit der Null soll die Einser geben.
Und die Zweier mit der Eins addiert, soll die Drei ergeben.
Und wir müssen jetzt wieder schauen, finden wir Gemeinsamkeiten zwischen diesen positiven Beispielen.
Und genau wie wir das vorhin auch hatten, haha,
wir haben hier Ausdrücke, die immer die Null als erste Argumentstelle haben.
Und wenn die erste Argumentstelle Null ist, was sehen wir denn da,
dann ist die zweite Argumentstelle gleich der dritten.
Das ist hier genau der Fall
Wir können es wieder überprüfen, einen komplizierteren Fällen,
wenn wir die Null mit der 2 addieren,
dann haben wir hier vorne eine Null.
Hier steht eine 2 und hier steht auch eine 2.
Das scheint eine Gesellschaftsmäßigkeit zu sein. Das heißt,
die erste Klausel, die wir haben, ist die,
die hier unten steht, das nimmt Open, sorry.
Dass die Null mit der Null mit irgendetwas addiert, ergibt wieder dieses irgendetwas.
Das ist der Regulsionsabschluss, den wir haben. Und wir
schauen jetzt wieder durch die Brille unserer Unifikation auf die Therme,
die wir hier haben und versuchen, einen dieser gegebenen Therme zurückzuführen,
auf einen einfacheren.
Und wir sehen also,
wenn wir hier eine S-Schale wegnehmen und hier eine Essschale wegnehmen,
dann haben wir es auf diesem Fall reduziert
Müssen wir testen, ob das Pattern funktioniert. Wir nehmen hier
eine Essschale weg und nehmen hier eine Essschale weg und haben erstmal nichts erreicht.
Wir führen zurück auf den Fall 1 plus 1 ist 2.
Wenn wir das Ganze aber nochmal anwenden,
dann nehmen wir hier die zweite Essschale weg und hier die zweite Essschale weg und siehe da,
wir haben es wieder reduziert, wir haben es reduziert auf diesem Fall,
hier bleibt nämlich eine Null übrig,
das ist unberührt und hier bleibt eine Eins übrig.
Dann haben wir durchwegnehmend von zwei Essschalen hier und hier
haben wir es auf diesem Fall 2 zurück geführt.
Und das schreiben wir hier unten hin. Wir nehmen beim ersten Argument eine Essschale weg,
wir nehmen beim zweiten Element eine Essschale, sorry,
beim Ergebnis einer Essschale weg und dann muss gelten,
dass das Innere dieser Pianozahl addiert mit der zweiten Pianozahl das Innere dieses Resultats ergibt.
Gut, und das können wir uns wieder vorstellen mit unserer Waage.
Nur diesmal soll die Waage nicht im Ungleichgewicht sein,
sondern im Gleichgewicht sein. Das ist der Unterschied.
Wir wollen ja Gleichheit zwischen den Dingen auf der linken Seite
und den Dingen auf der rechten Seite unseres Gleichheitszeichens haben.
Das gewünschte Verhalten ist ja, dass somant plus somant gleich
Summe sein soll. Und wir haben dann also eine Waage.
Wir müssen jetzt nur die Waage ein bisschen komplizierter machen,
dass wir eben hier die Waagschale unterteilen in den ersten Summanden und den zweiten Summanden.
Und das Ganze ist eben im Gleichgewicht,
wenn ich auf beiden Seiten gleich viele Kugeln habe.
Und hier beim ersten Summanden und beim zweiten Summanden beim Ergebnis
eine Kugel wegzunehmen, bringt die Waage nicht aus dem Gleichgewicht.
Die ist immer noch im Gleichgewicht. Wenn ich
die zweite Kugel hier wegnehme und hier wegnehme,
ist das Ganze immer noch im Gleichgewicht
Das ist die Idee dieser Rekursion, die wir hier aufbauen.
Wir nehmen immer beim ersten Sumanten und beim Resultat einer Essschale
weg. Das bringt die Addition nicht aus dem Gleichgewicht.
Wir können es dadurch zurückführen auf den Fall,
dass wir hier irgendwann mal null Kugeln haben.
Wenn wir hier null Kugeln haben,
dann müssen hier und hier gleich viele Kugeln liegen.
Also was der zweite Sumannt ist und was die Summe ist.
Das muss immer noch im Gleichgewicht sein. Und genau das
haben wir aufgeschrieben. Wir nehmen eben bei der ersten,
beim ersten Argument und beim Ergebnisargument
jeweils eine Essschale weg und das Gleichgewicht ist immer noch ungestört
Gut, wir können das Ganze wieder testen.
Wir fangen mit unterspezifisierten Aufrufen an. Wenn die funktionieren,
dann funktionieren die Spezifizierten auch, weil wir noch keine Negationen verwendet haben.
Oder weil wir, sagen wir es besser so,
weil wir die Suche noch nicht manipulieren.
Wenn wir das machen würden, dann können wir uns darauf nicht verlassen.
An die Fälle, die wir bisher betrachtet haben,
die die haben, alle die Eigenschaft.
Also erster einfacher Fall ist, zwei plus eins soll was sein.
Wir unifizieren mit dem Rekursionsabschluss, das gelingt nicht,
weil hier ein komplexer Therm hier auf ein Atom stößt,
das scheitert.
Wir unifizieren es mit dem Kopf des Rekursionsschritts.
Dann kriegen wir die Variablenbindung X1,
ist gleich S von
Das steht hier. Und wir kriegen die Variablenbindung X1 ist
gleich Y1. Moment. Warum habe ich das nicht hinge...
Ach Quatsch, ich bin falschen Film. Also,
wir kriegen die Variablen im X, ist S von Null.
Hier oben müssen wir ja unifizieren. Wir kriegen die Variablenbindung
Y1, ist gleich S von Null.
Das steht hier. Und wir kriegen hier hinten die Variablenbindung.
R1 ist gleich... Er ist von R1 ist gleich Summen
. Die Summe ist gleich S von
Auch hier sehen wir wieder den, den Effekt,
den wir jetzt schon mehrfach beobachtet haben.
Das Ergebnis wird hier bereits aufgebaut. Wir wissen noch nicht,
was R1 ist, aber wir kennen bereits das Äußere von Summen.
Die äußere Schale ist da. Und das ist der Trick
bei der Logikprogrammierung immer. Sie baut nicht wie die
funktionale Programmierung. Rekursive Ausdrücke von innen nach außen auf,
sondern sie baut sie von außen nach innen auf.
Und das werden wir noch sehen, führt dazu,
dass wir in der Logikprogrammierung Prädikate entrekursiv machen können,
was in der funktionalen Programmierung nicht geht.
Das gucken wir uns an, wenn wir zu den Listen kommen.
Das ist jetzt noch ein bisschen,
ich will es nur schon mal ankündigen
Auf diesen Effekt muss man immer achten.
Der ist entscheidend dafür, dass das ganze Endrekursiv wird.
Hier steht das Ergebnis, bereits partiell fest,
sodass wir wissen, es muss mindestens eine Essschale da sein.
Was im Inneren dieser Essschale passiert, das wissen wir noch nicht.
Das kriegen wir später. Gut, also mit dieser vaiablen
Bindung geht es jetzt in den Körper der Klausel hinein.
Das heißt, der Aufruf ist, Adire,
eins mit eins und gib mir das Ergebnis.
Und wir sehen, auch hier wieder Konsumtion auf einer Argumentstelle,
wir haben diese zwei hier zu eins vereinfacht
Wir unifizieren wieder mit dem Rekursionsabschluss. Das scheitert komplexer Thermen
. Hier, Atom hier, das geht nicht gut.
Wir unifizieren mit dem Kopf des Rekursionsschrittes.
Dann kriegen wir X2 an null gebunden. Y2 an S
von null gebunden und R1 an S von R2 gebunden.
Das sind die vajablen Bindungen, die wir hinten aufgeschrieben haben.
Und mit denen geht es jetzt wieder in den Körper der Klausel
hinein. Und da haben wir jetzt eben die Aufgabe,
addiere null mit 1 und gib mir
das Ergebnis
Jetzt ist plötzlich unser Rekursionsabschluss zuständig, denn hier kriegen wir eine Unifikation hin.
X3 wird ein S von 0 gebunden und damit wird R2 auch ein S von 0 gebunden.
Das ist der entscheidende Schritt. Und jetzt kennen wir,
dass die Co-Referenz, ja, die Pointerkette,
die Summe ist S von S von S von 0. Also,
das kriegen wir ausgegeben,
das Additionsergebnis von 2 und 1 ist 3. Und wenn wir jetzt nach Alternativen fragen,
dann wissen wir natürlich, Alternativen gibt es nicht.
Die interessante Frage ist eher,
warum gibt es die nicht
Wenn wir jetzt den Kopf der Klausel, des Rekursionsschrittes mit hinzunehmen,
dann haben wir hier wieder im Aufruf eine Null stehen.
Hier steht ein komplexer Term. Deshalb bringt uns der Rekursionsschritt
keine weiteren Ergebnisse. Und wir haben alle Entscheidungspunkte vollständig abgearbeitet.
Das heißt, das Ganze geht mit Ford zu Ende.
Weitere Ergebnisse gibt es also nicht. Das ist also gut
gegangen. Das ist ja die kanonische Verwendungsweise von Additionen,
gegeben. Zwei Semanten, gib mir bitte die Summe.
Natürlich müssen wir uns dafür interessieren, was passiert ist,
wenn wir das umgekehrt betreiben. Wir lassen jetzt eine der Summen,
eine der Semantenpositionen unterspezifiziert und geben eine Summe vor.
Wir fragen also, was muss ich zu zwei addieren,
sodass drei herauskommt
Und jetzt die gleiche Tour wieder durch. Hier vorne stehen
komplexe Therme. Das heißt, der erste und zweite Regussionsschritt bringt,
der klappt nicht mit den Fakten. Es sind immer die Regeln,
die uns diesen Thermen hier vorn vereinfachen, zu,
es von RX1 und dann zur, äh,
hier zu S von Null und dann zur Null,
also von zwei auf eins auf Null.
Und interessant ist jetzt nur, was passiert auf der zweiten Argumentposition.
Da passiert nämlich erst mal gar nichts. Da wird
immer nur Ko-Referenz hergestellt. Der Summant ist gleich X1,
äh, Y1,
Y1 ist gleich
Und Y 2, das ist gleich Y 3. Und das ist
der interessante Schritt. Hier sind wir nämlich beim Rekursionsschritt angekommen,
weil jetzt hier vorne eine Null steht,
addieren wir null mit irgendetwas, ist dieses irgendetwas.
Das heißt, jetzt haben wir Co-Referenz zwischen dem dritten Argument,
zwischen der dritten Argumentstelle und der zweiten Argumentstelle gegeben,
jetzt wird X Y 2 zu S von Null instanziert.
Und da wir hier oben nur eine Chor-Referenzkette aufgebaut hatten,
so mannt es gleich Y 1, ist gleich Y 2,
kennen wir jetzt den gesuchten Sumann.
Das ist nämlich die
Hier hinten ist dies eins produziert worden auf der dritten Argumentstelle,
indem wir von der drei eine Essschale abgebaut haben,
zur zwei und eine weitere Essschale abgebaut haben zur eins.
Und diese eins geht hier per Chor-Referenz an die zweite Argumentstelle
und ist der gesuchte Somant, den wir haben wollen.
Auch hier wieder der Test, wir schauen,
gibt es noch weitere Ergebnisse, das unifiziert nicht,
weil hier im Klauselkopf komplexer Therm und hier eine Zahl vor Licht.
Die Entscheidungspunkte sind wieder vollständig abgearbeitet, weitere Ergebnisse kriegen wir nicht.
Das heißt, es geht jetzt auch umgekehrt. Wir können
auf diese Art und Weise sagen, gut Deutsch, nichts anderes.
Was ist drei, minus zwei? Mit dem gleichen Programm
wohlgemerkt. Wir haben kein zweites Programm geschrieben. Es ist
immer noch das Additionsprogramm, aber die Umkehroperation funktioniert auch.
Und das ist klassischer Fall von Richtungsunabhängigkeit in der Logikprogrammierung.
Wenn ich das in der Einrichtung habe, besteht gute Hoffnung,
dass ich auch in der anderen Richtung haben kann.
Ich darf nur nicht an irgendeiner Stelle ein nicht logisches Konstrukt verwendet haben.
Und ein nicht logisches Konstrukt ist beispielsweise die Arrithmetik unseres Arrithmetikprozessors.
So wie ich auf die zurückgreife, scheitert das Ganze.
Habe ich nicht gemacht. Ich habe bisher nur Elemente der Logiprogrammierung verwendet,
nämlich Therme plus Klauseln, um einen Ableitungsprozess zu generieren.
Und solange ich in der Lage bin, das zu machen,
bin ich auf der sicheren Seite, dann funktionieren auch die Umkehroperationen.
Wir gucken noch die weiteren Unterspezifizierten Aufrufe an.
Funktioniert das auch, wenn ich das erste Argument unterspezifiziert lasse?
Also hier haben wir die Aufgabe, drei minus eins ist
was und dann kriegen wir eben die zwei heraus, das wollen wir haben.
Hier lassen wir jetzt mal zwei Argumente und instanziiert,
wir wollen zwei natürliche Zahlen so miteinander addieren,
dass zwei herauskommt und wir kriegen wie erwartet drei Ergebnisse,
addiere null mit der zwei,
addiere 1 mit der 1 und addiere zwei mit der null
Das sind die drei Möglichkeiten. Und weitere Möglichkeiten gibt es nicht.
Es sind genau die drei Varianten, die wir erwarten würden.
Das heißt also, offensichtlich haben wir hier eine richtungsunabhängige Spezifikation
eines arithmetischen Prädikats vorgenommen und richtungsunabhängig heißt eben,
es geht in allen Instanzierungsvarianten zu verwenden.
Glauben Sie mir, dass die anderen Instanzierungsvarianten auch funktionieren?
Die müssen wir jetzt hier nicht diskutieren.
Wir machen jetzt mal einen kleinen Exkurs zwischendurch,
dass wir uns auch mal um Programmiermethodik
wieder kümmern
Wir hatten ja eben gesehen,
dass wir in der Lage sind, Prädikatsdefinitionen aus Beispielen abzuleiten,
indem wir uns erstmal positive Beispiele überlegen,
die das gewünschte Verhalten illustrieren und wir dann diese Beispiele durch die Brille der Unifikationen angucken und Unifikation heißt eben für PA-Nothermen erstmal nur,
ich kann eine Essschale außen drum rum machen oder ich kann eine Essschale wegnehmen,
mehr kann die Unifikation nicht machen, solange die Therme so einfach sind.
Ich schaue durch die Brille der Unifikationen darauf und leite aus
dem Testbeispielen eine Prädikatsdefinition ab.
Wir können natürlich diese Testbeispiele jetzt natürlich auch zum Testen verwenden
In diesem Falle dann, das könnte man noch dazu machen,
das habe ich jetzt nicht gemacht, unterschieden nach positiven und negativen Testfällen.
Das sind die Testfälle, die wir aufgeschrieben haben.
0 mit 0 ist 0 und 0 mit 1 addiertes 1 und so weiter.
Und wir hatten auch negative Testfälle, die jetzt plötzlich auch verwendet werden können.
Also 0 mit 1 addiert soll nicht die Null sein.
Und wir können also diese Testfälle vor Beginn der Implementierungsarbeiten uns aufschreiben.
Wir können sie dann benutzen, um die Prädikatsdefinition abzuleiten.
Und wir können sie anschließend benutzen, um mit einer semi-automatischen Pestroutine
das Ganze überprüfen, ob es tatsächlich richtig gelaufen ist. Da,
so wie ich es jetzt gemacht habe,
die Tests nur für die spezifizierten Aufrufe verwenden,
müssen wir hinterher natürlich auch uns noch um die unterspezifizierten Aufrufe kümmern und möglicherweise die Testdatenbank sukzessive erweitern,
wenn wir sehen, dass wir in dem ganzen Prozess bei der Implementierung beispielsweise feststellen,
dass wir nicht alle Testbeispiele tatsächlich erfasst haben.
Also wir schreiben die Testfälle auf, ich habe einfach Einstelliges Prädikat hier,
Testdaten eingeführt und hier hinten stehen genau diese Testdaten,
die wir ursprünglich in der Tabelle hatten, drin.
Und dafür schraube ich, schreibe ich jetzt eine Prozedur.
Wir haben bis jetzt noch nicht alle Elemente, die wir
hierfür brauchen, tatsächlich behandelt, aber das macht nichts.
Ich kann trotzdem erklären, wie es funktioniert. Es ist
jetzt eine sogenannte fehlgesteuerte Routine, die immer verschiedene Sachen,
sie ruft, macht hier irgendetwas. Und wenn immer das positiv ausgeht,
sorgt dieses Fehl hier hinten dafür, dass dieser Körper der Klausel scheitert,
egal was davor passiert ist, Und dadurch,
dass er scheitert, zwingt er das System ins Backtracking.
Und jetzt muss das System gucken, wo habe ich noch alternative Entscheidungen zu treffen?
Wir werden das so schreiben, dass es keine Alternativen gibt.
Das ist ein extra logisches Prädikat, was irgendwas auf die Konsole schreibt.
Das hat sowieso keine alternativen Ergebnisse. Also die einzelnen Alternativen,
die getestet, die überprüft werden können, sind hier vorhanden.
Und das sind unsere Testdaten. Da sind sie. Dieses fehlt hier,
ups, dieses fehlt hier hinten, sorgt dafür,
dass sich die Testdaten ein nach dem anderen durchgehe,
mir mal auf die Konsole ausschreibe und dann einer Prozedur unterwerfe,
die die Herr Diagenoss heißt.
Und dass daher Gnoss das extra extrem einfach, dass du sagst,
nur rufe das, was du dort als Argument bekommst,
auf, das ist die sogenannte Metacall Facility,
gucken wir uns auch noch später an.
Das heißt, nur nimm den Wert,
der hier als Wert der Variablen kommt und rufe ihn auf.
Und das, was dort kommt, ist eben,
adiere 0 mit 0 mit 0.
Die Definition für Addition hat man ja inzwischen geschrieben. Das heißt,
er ruft dieses Teilziel auf, wenn das Ganze positiv ausgeht,
schreibt er Yes auf die Konsole aus.
Es ist positiv ausgegangen. Und wenn es negativ ausgeht,
dann schreibt er No auf die Konsole
Nichts mehr. Und ich kriege dann also eine Ausgabe,
jawohl, das hier ist positiv ausgegangen,
das ist auch positiv, das auch, das auch.
Hier ist ein negatives Ergebnis und hier ist ein negatives Ergebnis.
Das muss ich jetzt noch manuell überprüfen, das könnte man natürlich auch maschinell machen.
Ja, das muss man nicht manuell machen,
das könnte man in die Datenbank mit aufnehmen,
dass man hier gleich noch mit einführt.
Ja, ich habe ein zweites Argument, das steht dann
True or Falls drin und da muss ich nur überprüfen,
ob tatsächlich das Ganze zu Tour oder Falls ausgewertet wurde.
Und auf die Art und Weise kann ich also semi-automatisch
meine Testpferde durchspielen und schauen,
ob die zumindest die vollständigen Instanzierungsvarianten alle das gewünschte Ergebnis produzieren
Wie gesagt, was hier noch nicht mit dabei ist,
sind unterspezifizierte Aufrufe. Und wir haben ja gesehen,
dass die unterspezifizierten Aufrufe oftmals die Terminierungsprobleme hervorrufen.
Die muss ich natürlich gesondert testen. Auch das könnte man noch mit einbauen,
aber dann wird es kompliziert. Ich habe letzte Woche gerade gelernt,
von einem Übungszettel, den ich bekommen habe,
es gibt jetzt auch einen im SWI-Prolog, ein Unit-Testsystem,
das kannte ich bisher noch nicht, das ist neu dazugekommen.
Und wer also tatsächlich klassische Unit-Tests da durchführen möchte,
ist herzlich eingeladen, sich da mal einzuarbeiten.
Ich habe es auch noch nicht gemacht. Ich bin noch nicht
dazu gekommen, mir das mal genauer anzugucken. So,
ja, am Anfang dieses Abschnittes hatte ich ja irgendwie angekündigt,
wir machen jetzt noch drei Erweiterungen an der Syntax des Prologsystems.
Die erste Erweiterung waren die rekursiven Therme, die wir eingeführt haben.
Damit haben wir PANO-Zahlen kodiert. Die zweite Erweiterung,
die kommt jetzt, ist, dass wir Operatoren einführen und
die dritte nur zum Ankündigen werden die Listen sein.
Weil Listen häufig benutzte, rekursive Datenstrukturen sind,
gibt es dafür eine extra Syntax. Aber sowohl die Operatoren
als auch die Listen sind nur noch syntaktischer Zucker.
Das ist während, wie die rekursiv eingebauten Therme,
die brauchten wir. Die sind erforderlich, damit wir tatsächlich
so etwas simples wie natürliche Zahlen relational sauber implementieren können.
Die Operatoren und die Listen werden nur noch Notationsvarianten sein für
Dinge, die wir ohnehin schon haben.
Also, fangen wir mit den Operatoren an
Eine Notationsweise, die wir eigentlich schon kennen,
weil wir sie schon ganz, ganz oft benutzt haben,
bloß wir wussten es noch nicht, was wir da eigentlich tun.
Wir können die Syntax der Sprache modifizieren, dass wir einen Funktor,
den wir zum Beispiel bei der Addition jetzt eben,
war ja ein dreistelliges Prädikat, Ad mit drei Argumenten,
wir könnten dieses, nein, könnten wir natürlich nicht,
für zweistellige Prädikate, das geht nur für zweistellige Prädikate,
könnten wir einen Operator definieren,
den wir dann als Infix-Operator zwischen die beiden Argumente schreiben können.
Also, wir haben eine Möglichkeit,
ein Konstrukt zu definieren, das ein klassische Thermstruktur,
dieser Art hier ab sofort syntaktisch auch so geschrieben werden kann.
Mit dem Funktor als Infix-Operator zwischen den beiden Argumentpositionen.
Deshalb geht das natürlich nur für zweistellige Prädikate.
Das ist in der Mathematik ganz, ganz häufig der Fall.
Also gucken Sie in die Arrithmetik. Wie schreiben wir Additionen? Natürlich nicht,
indem wir plus und dann klammern und Argumente hinschreiben, sondern wir schreiben Additionen,
indem wir A plus oder 2 plus 3 hinschreiben als Infix-Operator.
Das heißt, wir hätten gern diese Notation,
weil wir sie gewöhnt sind,
Thermstrukturen haben aber immer intern
diesen Charakter an
Alles, was wir hier machen, ist reines Modifizieren der Syntax.
Wir ändern nichts an der Semantik, in keiner Weise,
ob er das Verarbeitungssystem bleibt, völlig unangetastet.
Wir haben ab sofort zwei alternative Notationsvarianten zur Verfügung.
Wir können ein Therm so schreiben, wir können ihn
aber auch so schreiben, wenn der Operator existiert.
Und damit er existiert, können wir ihn definieren.
Und fürs Definieren müssen wir immer angeben,
was ist es für ein, wie heißt der Operator,
was für ein Typ von Operator ist es.
Und wir müssen eine sogenannte Präzedenz angucken.
Wie heißt der Operator, das ist uns überlassen.
Wir können jeden beliebigen, jedes beliebige Atom als Namen für
einen Operator verwenden
Wir haben aber drei verschiedene Typen. Nämlich,
wie bisher schon diskutiert, der Infix-Operator,
das ist einer, der zwischen den Argumenten steht.
Also das, was wir von der Addition, der Multiplikation,
der Division und so weiter in der Mathematik schon kennen.
Wir schreiben den Operator zwischen die beiden Operanten, die er miteinander verknüpft.
Es gibt Prefix-Operatoren, die stehen vor dem Argument,
also aus der Mathematik bekannt, der Logerhythmus etwa,
den man als Prefix-Operator schreibt, geht nur für einstellige Strukturen.
Und das Gleiche gibt es auch als Post-Fix-Operator,
da steht der Operator nach seinem Argument,
Beispiel ist die Fakultät in der Mathematik
Ja, also vier Fakultät, da schreibe ich eben erst
das Argument und dann den Operator dahinter. Das sind die drei Typen,
die es gibt und die haben die Kürzel,
ich bin ein Prefix-Operator, ich bin das FS immer der Funktor.
Ich bin ein Post-Fix-Operator und ich bin ein Infix-Operator,
weil ich zwischen meinen Argumenten stehe.
Ich muss die für solche Operatoren, eine Operator-Präzedenz angeben,
das ist im Endeffekt die Frage, was bindet stärker?
Welcher Operator bindet stärker, wenn zwei Operatoren sind taktisch in Konkurrenz zueinander stehen?
Das kennen wir aus der Mathematik, also nein,
sorry, jeder Mathematiker würde mich erschießen für diese Aussage.
Das kennen wir aus dem Rechnen
Das kennen wir aus dem Rechnen, dass Punktrechnung vor Strichrechnung geht.
Heißt, Punktrechnung bindet stärker als Strichrechnung. Multiplikation bindet stärker
als Addition. Und das wird durch eine Operatorpräzedenz ausgedrückt.
Nur dummerweise ist es hier so, dass die geringeren Werte
eine stärkere Bindung bedeuten. Man hat irgendwann mal festgelegt,
dass der Wertebereich zwischen 1 und 1.200 liegt,
fragen Sie mich nicht, warum.
Das ist mir ein Buch mit sieben Siegeln. Kann aber sein,
dass es Systeme gibt, die sich daran nicht halten.
Das ist also möglicherweise implementationsabhängig. Die Implementation,
die ich kenne, verwenden alle 1.200 als maximalen Wert
Und dann heißt es eben, Punktrechnung bindet stärker,
weil es den niedrigen Präzedenzoperator kriegt,
der ist nur 400, im Gegensatz zur Addition ist 500.
Das heißt, die Multiplikation bindet stärker, weil es die kleineren Präzedenzwert hat.
Und deshalb muss ich zuerst die Multiplikation hier ausführen,
ehe ich die Addition ausführe. Und jetzt sind wir in der Lage,
Ausdrücke hinzuschreiben, wie es die Mathematik tatsächlich auch macht,
ohne lästige Klammern hinzuzufügen. Wenn wir uns sicher sind,
wie die Präzedenz aussieht, das ist natürlich immer Voraussetzung.
Und dann gelten die Klammerungsregeln der Mathematik ausdrücklich darauf hingewiesen,
das ist nicht Taschenrechner-Arhythmetik
Taschenrechner Arrithmetik kennt, im Normalfalle keine Klammerung. Es gibt
natürlich Taschenrechner, wo ich die Klammerung mit eingeben kann,
aber typischerweise ist das nicht der Fall.
Taschenrechner Arrithmetik würde eben erst A mit B addieren und dann mit C multiplizieren.
Was natürlich im Sinne der mathematischen Präzezedenz vollkommen mal kacke ist.
So, wir hatten bisher nur unsymmetrische Operatoren uns angeguckt.
Es gibt auch symmetrische Operatoren, die werden dann immer
so aufgeschrieben, dass hier gleiche Argumentstellen, Indikatoren stehen.
Und das hat dann bei zweistelligen Operatoren zur Folge,
dass es keine Operatoreinbettung geben kann
Also beispielsweise der Größenvergleich ist nicht syntaktisch einbettbar.
Ich kann in keiner Programmiersprache so etwas hinschreiben,
dass eins kleiner als zwei, kleiner als 3 ist.
Das ist nicht zulässig. Deshalb schreibt man das als nicht assoziativen Operator,
der genau das bereits vom Parser her für unmöglich erklärt.
Also im Falle eines solchen Ausdruckes würde der Prolog-Pause bereits sagen,
tut mir leid, Syntaxfehler, das geht so nicht.
Wir gucken uns als Beispiel unsere alte Relation,
wer liebt wen an, wenn wir das liebt als Operator definieren,
als zweistelligen Operator, und zwar als nicht assoziativen Infix-Operator,
dann können wir eben nicht so etwas hinschreiben
wie Hans liebt Geld,
liebt Susi
Das geht nicht, aber wir können sehr wohl ab sofort,
nachdem wir das definiert haben, verwenden.
Das liebt als Infix-Operator. Achso, ich habe nicht hinzugefügt,
das ist also die Definition für einen Operator.
Ich sage eben, ich definiere hier, liebt als Operator
mit Präzedenz 300 und vom Typ nicht assoziativer Infix-Operator.
Und ab sofort kann ich also das liebt zwischen seinen Argumenten verwenden.
So wie ich ein Additionszeichen da dazwischen schreiben könnte.
Und das ist semantisch völlig äquivalent zu dieser bekannten Thermstruktur
mit liebt als Funktor und den Argumenten in Klammern
Wenn ich jetzt mir das Palisting ausgebe, das ist die Definition,
die ich einlese. Wenn ich das mir Palisting ausgeben lasse,
dann kriege ich eben heraus, dass dann wird es immer so ausgegeben,
wie ich es ja gewünscht habe, nämlich als Operator,
in Operator Notation.
Tatsächlich hinter den Kulissen ist es die alte Functo-Argumentstruktur.
Wenn ich es als Klauselkörper, das heißt also als Aufruf verwende,
als Teilziel verwende, dann kann ich eben beide Varianten verwenden.
Ich kann es als Infix-Operator benutzen. Ich kann es
als Functo-Argumentstruktur aufrufen.
Das geht beides
Also es ist nur eine syntaktische Notation, die manchmal günstig ist.
Und wir hatten schon die Möglichkeit der rekursiven Einbettung.
Da müssen wir noch wieder zwei Typen unterscheiden, nämlich ob die Einbettung links,
rekursiv oder rechtsrekursiv verfolgt. Also links, assoziativ heißt,
dass der Baum dieser Ausdrücke immer nach links expandiert,
während rechtsassoziativ heißt, dass dieser Baum nach rechts expandiert.
Das wäre also Äquivalent zu einer Struktur, die immer die letzten beiden Argumente miteinander klammert.
Also die erst das A mit dem B addiert,
das ist dieser Teilbaum hier,
die dann zum Ergebnis dieser Addition das C hinzufügt, das ist dann dieser Teilbaum.
Und als letztes, das D hinzufügt. Im Gegensatz zu Rechtsassoziativ,
die eben sagt, ja, ich soll A mit irgendetwas addieren,
also muss ich dieses irgendetwas erst ausrechnen.
Wenn ich das mit B addieren soll, dann muss ich erstmal dieses irgendetwas ausrechnen.
Und das kann ich tatsächlich machen. Also Rechtsassoziativ heißt,
ich fange in einer Sequenz A plus B plus C plus D,
zuerst an die rechten Sumanten zusammenzufassen,
ehe ich es mit dem links davonstehenden Verknöpfe.
Während links assoziativ heißt, ich fange in der Reihenfolge von links
nach rechts an, das Ergebnis zu berechnen.
Wenn wir jetzt in die Liste der Operatoren gucken,
dann kommen uns zahlreiche alte Bekannte auf einmal unter.
Wir finden als allererstes mit der niedrigsten Präzedenz unseren Klauselkopf-Separator,
der Doppelpunkt minus, den wir benutzt haben, um Klauseln aufzuschreiben.
Wir erfinden etwas geringere Präzedenz, die Dissiunktionen, unser Simikolon,
mit noch geringerer Präzedenz finden wir das Komma,
dem wir Teilziele in der Klausel voneinander trennen können.
Wir finden hier auch die Direktiven, auf der gleichen,
nicht zufälligerweise, auf der gleichen Präzedenzebene, wie der Klauselkopf-Separator,
allerdings mit der anderen Struktur, das hier ist ein Infix-Operator,
klar, er trennt Kopf von Körper der Klausel.
Das hier ist nur ein einstelliger Operator, wo ich hier
dahinter eben nur ein Teilziel angeben kann. Wir erinnern uns,
das tritt üblicherweise in Programmen auf, wenn ich Prädikate definiere,
das hier tritt nur in Form von Direktiven auf,
wenn ich dem System sagen will,
werde ich irgendwelche Parameter im System setzen will.
Wir hatten es bisher nur dafür, dass man beispielsweise sagen kann,
dies ist ein dynamisches Prädikat und darf zur Laufzeit verändert werden.
Dann geht es weiter mit dem Spaß.
Das ist natürlich schlecht. Das Not ist kein Operator mehr
. Sorry, das habe ich noch nicht gestrichen.
Aber dieses Backslash Plus ist ein Prefix-Operator. Die Unifikation ist
auch nichts anderes als ein Operator. Auch natürlich die negative Unifikation,
dass es nicht unifizieren darf, ist ein solcher.
Dann die hier müssen wir uns gesondert angucken. Die überspringe
ich jetzt mal. Der numerische Vergleich in all seinen Varianten
ist ein Infix-Operator. Struktureller Vergleich kommen wir auch gleich dazu.
Ist auch ein Infix-Operator. Alle arathmetischen Funktionen, die wir haben,
also plus minus mal geteilt, potenzieren und so weiter,
sind Infix-Operatoren. Auch ein Operator namens Modulo. Achso,
nee, halt, das ist die Intager-Devision, sorry.
Das hier ist das Exponentieren. Ja, gut,
was uns das sagt, ist, dass offensichtlich das,
was wir bisher immer mit dem Klauselkopf-Separato geschrieben haben,
nur eine spezielle Struktur ist.
Also, wenn wir eine Regel hinschreiben,
dass wir ein Großvater definieren über die Fahrt davon Beziehung,
dann ist das vollständig Äquivalent dazu, dass wir eine zweistellige Funktor haben.
Nämlich, das ist unser Kopfseparator. Das erste Argument ist
der Kopf der Klausel und das zweite Argument ist der Körper der Klausel.
Der Körper der Klausel ist natürlich wieder ein komplexes Ziel.
Das heißt, der Funktor ist die Konjunktion und die Argumente
sind die beiden Teilziele, die zu erfüllen sind.
Das, wir könnten Programme auch auf diese Art und Weise schreiben.
Tatsächlich sind die ersten, die ersten Prolog-Systeme auch in der
Art gewesen und entsprechend schwer handhabbar.
Das ist natürlich erheblich einfacher, dass man hier diese Operator Notation einführt.
Aber interessant ist eben, dass alles,
was wir bisher hatten, tatsächlich auch Operatoren sind.
Die Botschaft ist, es gibt im Prologsystem nichts anderes als Thermstrukturen.
Alles ist eine Thermstruktur. Und das heißt,
insbesondere Daten und Programme sind taktisch an keiner Weise zu unterscheiden.
Der einzige Unterschied ist, wo treten Daten auf,
wo treten Programme auf. Programme muss sich in die per
Assert in die Datenbank hineinschreiben, Daten treten als Argumentbelegung auf.
Aber ansonsten ist es syntaktisch vollkommen gleich und heißt natürlich auch,
ich kann jederzeit und da kommen wir bei der Metaprogrammierung drauf zurück,
kann jederzeit den Übergang von Daten zu Programmen und von Programmen
zu Daten
Also ich kann Programme als Daten verwenden und ich kann Programme erzeugen,
weil sie ja Daten sind und sofort abarbeiten.
Das ähnelt also der Welt, der funktionalen Programmierung ganz,
ganz stark. Da kommen wir aber später drauf. Ja,
der eine Operator, wo ich drüber hinweggegangen bin,
ist dieses Add kleiner als, das ist ein sogenannter struktureller Vergleich.
Damit kann ich beliebige Therme vergleichen. Während mit
dem normalen kleiner Alt ich nur Zahlen miteinander vergleichen kann,
also zum Beispiel drei kleiner als vier,
kann ich mit diesem Operator beliebige Daten vergleichen.
Und damit das möglich ist, gibt es eine sogenannte Standardordnung
der Therme
Und die sagt, das Kleinste, was wir im System haben,
sind unsere Variablen, die lexikalischen Atome sind,
die größeren, die zahlen als spezielle Atome,
sind noch größer als die lexikalischen Atome und die Strukturen sind die größten Elemente.
Die Atome intern werden nach Zeichencode lexikalisch sortiert, also Aski-Code-Sortierung,
die zahlen nach ihrem numerischen Wert ohne Typunterscheidung,
also egal ob Gleitkomma-Zahl oder Intacher-Zahl,
und die Strukturen dann wieder rekursiv nach Funktor und Funktor muss arithmetisch,
muss lexikalisch sortiert werden,
Stelligkeit und dann die Sortierreihenfolge der Argumente
Also so etwas wie Vagable A ist kleiner als A.
Es gibt natürlich ein Truh, das sind nur positive Beispiele hier.
Ein Atom bestehend aus 3x A ist kleiner als ABC,
weil das lexikalisch vor dem hier kommt.
Ein Atom ist kleiner als eine Struktur. Strukturen sind immer
die größten Elemente, die wir hier haben. Eine,
wenn wir Strukturen aufeinanderstoßen, dann ist eben das mit dem
kleineren Funktor kleiner als das mit dem größeren Funktor.
Wenn denn die Funktoren gleich sind, dann gewinnt das,
was die höhere Stelligkeit hat
Und wenn denn die Schnelligkeit auch gleich ist, dann gewinnt das,
was irgendwo, nein, was als allererstes, ein höheres Argument hat.
Auf der ersten Argumentstelle ist auch noch Gleichheit gegeben,
aber auf der zweiten Argumentstelle trifft ein C auf ein D.
Deshalb ist dieser Term hier kleiner als dieser Term.
Der wesentlichste Spezialfall, wofür man diesen ganzen Zirkus braucht,
ist natürlich der lexikalische Vergleich. Also genau diese Stelle hier,
wenn ich mal Atome nach ihrem lexikalischen Wert sortieren möchte.
Da braucht man es, warum man irgendwo vajablen mit Atomen
vergleichen sollte und da Weiß ich nicht. Das Nette ist,
und das können wir uns merken, das Nett an der Sache ist,
dass das eben nicht nur für Atome gilt, sondern auch für Strukturen.
Wir können also beispielsweise Strukturen aufbauen im Find All und dann sortieren
lassen nach der ersten Argumentstelle. Also das ist ein beliebter Trick,
wenn man mehrere Ergebnisse aggregiert hat,
dass man die gleich von vornherein so aufbaut, dass sie,
dass das der Größenunterschied auf der ersten Argumentstelle ausgedrückt wird.
Dann brauche ich nämlich nur ein Zord drüber laufen zu lassen und
dann sind die Dinger nach ihrer, nach dieser Argumentstelle sortiert.
Das ist eine Konsequenz dieses Vergleichsoperators
Gut, also was wir gesehen haben, Strukturen sind die einzig verfügbare Repräsentation,
die wir haben in der Logik-Promierung etwas anderes als Strukturen gibt es nicht.
Operator, Operatoren sind nur, sind taktische Notationsvarianten,
die wir benutzen, damit die Sachen leichter lesbar und bequemer handhabbar sind,
aber sie sind keine extra Konstruktion im gesamten Gebilde der Programmiersprache.
Wir haben Programme und Daten, vollkommen mit den gleichen Datenstrukturen dargestellt.
Die sind so taktisch nicht unterscheidbar voneinander. Und das hat zur Folge,
dass auch, dass ich da ein Prädikatsdefinition und Prädikatsaufrufe dynamisch
zur Laufzeit erzeugen kann und wechselseitige Umwandlung zwischen Daten und Programmen ist möglich.
Wir haben das bereits in einem Fall gesehen. Das war
die Meta-Call Facility beim Testen unserer, das beim Testen unserer Ausdrücke.
Dies hier ist nichts anderes als eine Kurzschreibweise von Co.,
Klammer auf, X, Klammer zu.
Und dieses Cork, Klammer auf, irgendetwas, ein Ziel,
Das ist eben, ich rufe ein Datenelement,
was sich gerade konstruiert habe, auf als Programm.
Das erzeuge mir ein Programmaufruf. Oder ich kann eben auch
Programmklauseln als Daten verwenden. Da gibt es auch ein eingebautes Prädikat.
Das sagt mir, gib mir eine Klausel, wo das
der Kopf und das der Körper ist. Oder mit anderen Worten,
suche mir aus der Datenbank, das ist ja beides instanziert aufrufbar,
suche mir aus der Datenbank alle Klauseln,
die mit diesem Ausdruck unifizieren, wo der,
sorry,
wo der Kopf,
der Klausel mit diesem Ausdruck unifiziert und gib mir
die Körper der Klausel zurück
Das werden wir dann bei der Meta-Programmierung ausnutzen. Ist hier nur angedeutet,
dieser Unterschied zwischen Daten und Programmen verwischt weitgehend und das sind
die beiden Verbindungselemente, die wir später brauchen werden.
Das kann jetzt noch nicht klar sein, dass es nur
eine Vorbereitung auf das, was später kommt. So,
nun haben wir uns bisher ja mit einer relationalen Programmierung,
relationalen Definition von Arithmetik befasst, indem wir die Intergrae-Arithmetik,
nein, falsch, die natürliche Zahl Arhythmetik mithilfe von rekursiven Thermen implementiert haben.
Ich hatte es ja vorhin schon angedeutet, aus Effizienzgründen,
die denkbar schlechteste Art und Weise Arhythmetik zu implementieren.
Allerdings ganz nett dadurch, weil wir sehen können,
was heißt eigentlich wirklich Richtungsunabhängigkeit in der Logikprogrammierung.
Wir wollen natürlich, wenn wir effiziente Programme schreiben,
irgendwann mal den Service benutzen, der in jedem unserer Rechner drinsteht.
Und dieser Service heißt Arhythmetikprozessor. Der kann diese Operatoren,
Operationen natürlich sehr schnell, sehr effizient durchführen und wir werden
schön dumm, wenn wir auf diesen Service nicht zurückgreifen wollten
Die Frage ist also, ja, können wir die Arithmetik,
nicht auch, wie es in der funktionalen Programmierung,
wie es in der imperativen Programmierung passiert, funktional durchführen,
indem wir eine Funktion angeben und deren Funktionswert berechnen,
das können wir eben nur,
wenn wir das Logikparadigma verlassen und wir bauen jetzt in der Welt der Logikprogrammierung eine spezielle Ecke auf,
die nur zu dem Zweck dient,
arithmetische Funktionen auszuwerten,
die sofort scheitert,
wenn ich an dieser Stelle irgendetwas anderes mache als Arithmetik und die sofort scheitert,
wenn ich
die Basis der funktionalen Programmierung verlasse
Das ist also jetzt in der Welt der Logikprogrammierung eine Spezialecke,
die nur für die Auswertung arithmetischer Funktionen zuständig ist.
Dazu müssen wir erst mal klären, was ist eigentlich
das Verhältnis zwischen Funktionen und Relation an der Stelle.
Bisher haben wir ja Operatorausdrücke und Strukturen, die stehen immer
nur für sich selbst. Sie haben ja keinen Wert.
Ich kann Strukturen nur miteinander unifizieren und kann feststellen,
ob die Unifikation gelingt und wenn ja, unter welchen Bedingungen sie gelingt.
Das ist alles, was ich machen kann. Wenn ich
jetzt so etwas wie eine Werte aus Werteermittlung einführen will,
dann muss ich eine Umgebung schaffen, die eben Werteermittlung zulässigt ist, die das erlaubt.
Und das dann, erst wenn ich das habe,
will ich in der Lage tatsächlich einen solchen Ausdruck einzugeben und zu sagen,
bitte gib mir den Wert dieses Ausdrucks zurück und prüfe nicht,
ob er mit irgendetwas unifizierbar ist.
Das ist die Basis der Logikprogrammierung gewesen.
Das ist das Ganze als Prädikat. Ich habe zwei Input-Argumente,
ich habe ein Output-Argument. Hier habe ich zwei Input-Argumente und
ich kriege einen Funktionswert als Output dazu
Also, typischerweise,
wenn ich eine zweistellige Operator- und Struktur in der funktionalen Welt habe,
habe ich ein dreistelliges Prädikat in der relationalen Welt,
weil ich ja natürlich die Ergebnisposition irgendwo brauche.
Denn ich kann es ja nicht funktional auswerten.
Wenn ich also mit diesen arithmetischen Operatoren arbeiten will,
dann muss ich eine Umgebung haben, die sagt,
wenn eine solche Struktur da ist, ermittle ihren arithmetischen Wert.
Und damit haben wir lokal ein funktionales Verarbeitungsmodell in dem großen,
in der großen Umgebung der relationalen Programmierung
Fototyp der arithmetischen Auswertungsumgebung ist ein Infix-Operator, der heißt IS.
Und der hat eine spezielle Semantik,
sonst wäre er nicht geeignet als Ostwertungsumgebung. Jeden Ausdruck,
den ich unter diesem Operator als rechtes Argument dieses Apparators angebe,
der wird ausgewertet. Und die Struktur ist also immer,
ich habe hier links eine Zahl und hier rechts einen arithmetischen Ausdruck.
Und diese Mantik ist, werte diesen Ausdruck aus und unifiziere
mit dem, was auf der linken Seite steht
Diese Zahl muss nicht instanziiert sein. Die rechte Seite muss instanziert sein,
wenn hier eine nicht instanzierte Variable drinsteht, dann scheitert dieses Prädikat.
Denn das ist das Wesen funktionale Auswertung. Ich muss alle
Inputargumente kennen, sonst kann ich den Funktionswert nicht ermitteln.
Und das guckt an der Stelle natürlich durch.
Das werden wir nicht los. Das hier kann uninstanziert aufgerufen
werden. Das heißt, hier kann eine Variable stehen.
Hier darf eine Variable oder eine numerische Konstante stehen.
Hier müssen arithmetische Ausdrücke stehen, die können Variablen enthalten,
aber die Variablen müssen alle bereits instanziert sein. So,
und das Sind alles Konsequenzen der funktionalen Verarbeitungsweise unserer Arrimetikmaschine,
die wir haben, der müssen wir Rechnung tragen.
Ansonsten können wir sie nicht benutzen. Und das heißt,
auf der linken Seite der Auswertungsumgebung ist keine Struktur zulässig,
nur eine Variable oder eine numerische Konstante.
Auf der rechten Seite muss der Ausdruck sich zu einem arrithmetischen Wert auswerten lassen.
Ansonsten gibt es einen Sevantik-Fehler und das System sagt,
ich kann es nicht.
Die rechte Seite darf deshalb Variable nur enthalten,
wenn sie
vollständig instanziert sind
Konsequenz ist, wann immer ich diese Auswertungsumgebung in einer Prädikatsdefinition verwende,
dann darf ich nicht mehr erwarten,
dass ich die Präkretikatsdefinition richtungsunabhängig ist.
Die geht über den Jordan bei der Gelegenheit.
Also die richtungsunabhängigkeit geht verloren in dem Augenblick,
wo ich an irgendeiner Stelle mal die arithmetische Auswertungsumgebung bemühe.
Das ist, schlägt viel, wenn die Typrestriktion verletzt sind.
Also zum Beispiel die linke Seite der Auswertungsumgebung ein
komplexer Term ist oder wenn die Unifikation zwischen linker und rechter
Seite nicht funktioniert
Oder das Ganze bricht mit einem Fehler ab,
wenn die rechte Seite sich nicht auswerten lässt,
wenn also dort kein arithmetische Funktionen angegeben ist oder wenn sie uninstanzierte Variablen enthält.
Und in gewisser Weise ist die Istanweisung,
oder der ist Ausdruck, etwas ähnliches wie die Ergibtanweisung imperativer Sprachen,
aber es ist ein bisschen mehr, weil sie auch noch die Testbedingung ermöglicht.
Es ist nicht nur das Zuweisen eines Wertes,
ich kann es benutzen, um eine Wertezuweisung zu machen,
aber es ist nicht die einzige Verwendungsweise,
ich kann es auch als Testoperator verwenden
Also hier ein paar Beispiele. Das erste ist so ein Testoperator,
ist die drei auf der linken Seite gleich dem arrethmetischen Wert auf der rechten Seite.
Und dann wird eben hier zweimal 5, minus 7
ist drei ausgewertet und das ist unifizierbar mit dem drei.
Also gibt es ein True an der Stelle.
Wenn ich hier auf der linken Seite eine Variable habe,
dann wird eben die rechte Seite ausgewertet und der Wert dieser
Auswertung wird der Variablen zugewiesen.
Das ist genau die Ergibtanweisung der imperativen Programmierung
Wenn ich auf der linken Seite einen komplexen Therm habe,
dann wird der nicht ausgewertet, sondern dann habe ich hier eine Struktur,
hier habe ich eine Zahl als Berechnungsergebnis,
die beiden kann ich nicht miteinander unifizieren,
deshalb kommt ein Fail daraus.
Wenn ich nivayable X auf dem Wert 5 setze und anschließend fordere,
dass die vayable X gleich dem Produkt aus 2 und 6 sein soll,
dann scheitert das,
weil dieses X bereits den Wert 5 hat und deshalb ist die 12,
die ich hier ermittle,
nicht mit der 5 Unifizierbar
Wenn ich das auf eins setze und auf der rechten Seite,
in dem zweiten Ausdruck,
das X, was er bereits im Wert eins hat,
um eins erhöhe, dann fordere ich hier,
dass eins gleich zwei sein soll und das kann nicht klappen.
Ich möchte auf diesen Fall ausdrücklich hinweisen,
denn das ist das Kontrastprogramm zur emperativen Programmierung.
Es ist ja die häufigste Verwendungsweise einer Ägypteranweisung, dass ich sage,
X ist gleich X oder Sätze X auf X plus 1,
da wo ich eine laufvaiable hochaddiere,
was eben dann in der objektorientierten Programmierung das plus plus 1
geworden ist
Dass ein solcher Ausdruck scheitert hier immer systematisch. Werde ich in dem gleichen Ausdruck,
das durch die Multiplikation ersetze, gelingt es plötzlich.
Warum? Ja. Weil im mathematischen Sprichweise die Eins,
das Nullelement der Multiplikation ist, die ändert an dem Faktor nichts,
wenn ich ihn mit Eins multipliziere.
Das heißt also, was immer für einen Wert X hat,
es ist wieder X und damit gelingt diese Unifikation hier dazwischen
Gut, wir müssen diese, dieses Scheitern,
was ja ein ganz normaler Vorgang in der Logikprogrammierung ist,
sauber unterscheiden von semantischen Fehlern, die auftreten können.
Also Fehler sind natürlich Dinge, die die Verarbeitung zum Abbruch bringen.
Das Fail heißt nur, diese Variante ist nicht erfolgreich gewesen,
aber vielleicht gibt es ja Alternativen dazu. Ein Fehler tritt also immer auf,
wenn ich hier einen solchen Ausdruck eingebe,
wo das Y uninstanziert ist,
dann kriege ich hier Arguments are not sufficiently instantiated als Fehlernachricht
oder hier kriege ich dann eben A is not a function,
heißt A,
ist keine arithmetische Funktion und deshalb
nicht auswertbar
Im Gegensatz, wenn ich hier etwa XSP hinschreiben würde,
Pi ist eine arithmetische Funktion.
Mit einem konstanten Funktionswert, das ist auswertbar. Also da,
das ist dann natürlich abhängig von dem Ausdruck, der da steht.
Es ist ja ein semantischer Fehler, es ist ja kein syntaktischer Fehler.
Es gibt weitere Auswertungsumgebungen im System, die haben wir auch
schon teilweise gesehen. Das sind die Größenvergleiche,
das ist ein spezieller arithmetischer Vergleichsopperator, das ist nicht die Unifikation,
sondern das ist eine funktionale Auswertungsumgebung, die sagt,
Werte links aus,
werte rechts aus und vergleiche das arithmetisch
Genauso hier, werte links aus,
werte rechts aus und vergleiche es bezüglich der Größenrelation.
Und auf die Art und Weise kann ich jetzt also testen,
ist zehn geteilt durch zwei arhythmetisch gleich vier plus eins.
Und das ist Tou, weil eben links zu fünf
auswertet und bei rechts zu fünf auswertet.
Zehn minus zwei ist nicht gleich vier mal eins,
weil links zu acht auswertet und rechts zu vier auswertet.
Deshalb ist arhythmetische Gleichheit nicht gegeben und
das Ganze scheitert
Diese Operator, diese Vergleichsoperatoren und insbesondere eben diese arithmetische Gleichheit,
kann nicht zur bariablen Instanzierung verwendet werden.
Also wenn ich es fordere,
setze X auf den Wert von 2 mal 3 minus 1,
dann kommt genau dieser Fehler.
Denn hier ist eine uninstanzierte Variable verwendet worden.
Das scheitert, weil dieses Argument nicht instanziert ist.
Wenn ich eine vaiable Instanzieren will, dann muss das
mit dem Ist passieren.
Aber nicht mit diesem arithmetischen Vergleichsopparator.
Zusammenfassung. Für die Arrithmetik haben wir also eine extra Umgebung
und damit ist unser Prologsystem eigentlich streng genommen wieder eine hybride Sprache,
weil sie funktionale Auswertung in einer relationalen Umgebung durchführt.
Und weil sie Restriktionen hat, die die nicht deklarative Semantik
unseres Arrithmetikprozesses widerspiegeln und aus dem relationalen Modell herausfallen.
Und die Ursache dafür ist,
dass hier in der Maschine eben die Arrithmetik mit außerlogischen Mitteln
behandelt wird und
nicht mit logischen Mitteln
Was hybride Sprache heißt hier nicht, nicht,
dass das irgendjemand, der den falschen Hals kriegt,
dass ich jetzt in der Lage wäre, hier funktional zu programmieren.
Das bin ich mitnichten. Ich bin nur in
der Lage, existierende arithmetische Funktionen auszuwerten.
Ich kann keine neuen und schon gar keine rekursiven arithmetischen Funktionen definieren.
Das geht nicht. Also insoweit geht die Hybridizität nicht,
dass ich jetzt in dieser Umgebung tatsächlich eine eigene arithmetische Funktion definieren könnte.
Aber ich kann sehr wohl die eingebauten Funktionen,
die jedes Programmiersystem enthält, hier benutzen und dann entsprechend
die Ausdrücke auswerten
Gut, das soll für heute genug sein.
Ich danke Ihnen für die Aufmerksamkeit.
Ich wünsche Ihnen ein schönes Wochenende und bis nächsten Freitag