So, einen schönen guten Tag. Wir hatten uns vor die Woche damit beschäftigt.
Arrithmetik auf logischer Basis zu rekonstruieren, zumindest in Teilen,
viel weiter als bis zur Addition der natürlichen Zahlen sind wir ja nicht gekommen.
Das ist eine Implementationsvariante, die logisch sauber ist.
Deshalb die Vorteile der Richtungsunabhängigkeit mit sich bringt. Also insbesondere,
dass sich die Umkehr Rechenoperationen sofort mit der gleichen Implementierung erledigen kann,
wie die Operation selber,
also Addition und Subtraktion als Umkehroperation mit einem Programm bewerkstelligen kann,
aber dafür den Nachteil hat, dass sie extrem ineffizient ist.
Also insbesondere Speicherplatz in extremer Weise verbraucht. Deshalb hatten wir das erweitert,
oh nein, haben wir den Übergang gemacht,
dass wir eine spezielle Auswertungsumgebung einführen, die es uns erlaubt,
arithmetische Ausdrücke auszuwerten, das heißt Funktionen ihren,
für eine gegebene Funktion, einen Funktionswert zu ermitteln.
Das ist eine gesonderte Umgebung innerhalb der Logikprogrammierung,
weil wir in dem Augenblick, wo wir das verwenden,
die Basis der Logikprogrammierung verlassen und uns in die funktionale Programmierung begeben.
Oder zumindest in die Auswertung funktionaler Ausdrücke begeben.
Der einfachste Fall dieser Umgebung ist der zweistellige Operator ist,
ist ein asymmetrischer Operator, der also auf der linken Seite
andere Restriktionen kennt als auf der rechten Seite.
Auf der rechten Seite muss also tatsächlich etwas stehen,
was sich als arithmeter für Ausdruck auswerten lässt,
wann immer diese Auswertung schief geht, kommt es zu einem Fehler.
Auf der linken Seite darf ebenfalls nur eine Vagable beziehungsweise eine Zahl
stehen und nach Auswertung der rechten Seite wird die linke und die rechte Seite miteinander unifiziert.
Das ist die Semantik, diese speziellen Opperators.
Der bringt also eine ganze Reihe von Restriktionen mit sich,
aber das sind eben keine Restriktionen der Logikprogrammierung,
sondern das sind die Restriktionen unseres Arrithmetikprozessors.
Der Arrithmetikprozessor arbeitet eben nicht relational, sondern funktional.
Gut, also die Restriktionen sind hier nochmal zusammengefasst.
Die linke Seite darf keine Struktur sein, sie kann es
natürlich sein. Niemand verbietet ihnen da eine Struktur hinzuschreiben,
nur dann können sie sicher sein, dass die Unifikation immer scheitert.
Dann können sie gleich Fehl dorthin schreiben, weil es keine Möglichkeit gibt,
eine solche Unifikation mit einer Zahl gelingen zu lassen.
Die rechte Seite, bereits gesagt, muss sich auswerten lassen
zu einem arithmetischen Wert. Sie darf vaiable enthalten,
allerdings müssen die Variablen vorher instanziert worden sein,
sodass bei der Auswertung ein numerischer Wert zur Verfügung steht.
Wir können nicht uninstanzierte Variablen dort benutzen. Das führt
auch zu einem Fehler. Und das sind alles Fehler,
die die Verarbeitung abbrechen. Das ist nicht,
dass das Programm scheitert, sondern das ist dann ein
harter Fehler
Ja, gut. Und die Konsequenz davon ist dann eben,
wann immer ich diese Auswertungsumgebung verbände, dann sind die entsprechenden Prädikate,
die ich definiere, nicht mehr richtungsunabhängig.
Dann erbt sie die Richtungsabhängigkeit von dieser Auswertungsumgebung. Es gibt
andere Stellen, wo sie versteckt sind. Das sind etwa,
ist diese arithmetische Vergleichsopperator, der ein arithmetisches Gleich testet.
Es wird auf der linken Seite arithmetisch ausgewertet,
es wird auf der rechten Seite arithmetisch ausgewertet und dann werden die beiden Auswertungsergebnisse,
entweder auf Gleichheit, auf kleiner als oder auf größer als getestet.
Das Gleiche natürlich für die Kombination kleiner, gleich,
größer, gleich. Diese Operationen sind also nicht geeignet,
Variableninstanzierung vorzunehmen, das geht einfach nicht,
wenn auf einer der beiden Seiten eine uninstanzierte Variable auftaucht,
ist das auch eine Fehler, ein Fehlerfall.
Wenn ich eine Variable instanzieren will, dann muss ich,
die ist Umgebung verwenden, die unsymmetrische Umgebung,
die ist mir erlaubt,
auf einer Seite auszuwerten und dann per Unifikation einen Wert an
eine Variable
zu übergeben
So, damit haben wir die hybride Sprache und soweit waren wir letzte Woche gekommen.
Wir haben in einer insgesamt relationalen Umgebung eine spezielle Ecke aufgemacht,
die sich jetzt mit funktioneller, funktionaler Auswertung befasst und
die zusätzliche Restriktion des funktionalen Modells dort mit sich bringt.
Jetzt gucken wir uns mal an, was wir damit machen können,
indem wir eine Reihe von Beispielen uns anschauen.
Der einfachste Fall ist natürlich, ein Beispiel zu nehmen,
wo wir bereits eine rekursive Berechnungsvorschrift vorliegen haben.
Also ich überspringe den Fall wo wir eine analytische,
geschlossene Formel haben, die wir nur entsprechend in Prolo kodieren müssen.
Das ist der triviale Fall. Das ist wie in jeder
anderen Programmiersprache. Der interessantere Fall ist,
wo ich also eine Berechnung tatsächlich über mehrere Prozeduraufrufe machen muss.
Und das sind die speziellen interessanten Fälle, wo das eine rekursive Prozedur ist.
Eine solche rekursive Prozedur ist die Berechnung der Fakultät.
Jeder weiß, wie die Berechnungsvorschrift ist. Die Fakultät berechnet sich also,
und das ist eine funktionale Spezifikation, zu eins,
wenn N gleich Null ist und zu N-1-Fakultät mal N sonst.
Und damit haben wir die Rekursion,
wir müssen die Berechnung der Fakultät N auf die Fakultät
von N-1 zurückführen
Und dann diese Berechnung wird natürlich wieder auf die Berechnung der jeweiligen,
dass ihr bei dem Vorgängerwert es zurückgeführt,
bis sich irgendwann mal bei der Eins angekommen bin und dann dort eine Null zur Verfügung habe.
Funktionale Spezifikation heißt immer, ich habe eine einstellige Funktion hier.
Wir haben ja die Ergebnisübergabe in der funktionalen Programmierung über den Funktionswert.
Der steht uns ja in der relationalen Programmierung nicht zur Verfügung.
Deshalb gilt als Faustformel immer,
wenn ich eine funktionale Spezifikation in eine relationale überführen will,
brauche ich eine zusätzliche Argumentstelle, um das Ergebnis zur zurückzubekommen.
Das heißt, die relationale Definition wird jetzt eine zweistellige Relation sein,
weil ich hier eine einstellige Funktion habe.
Und diese Relation beschreibt jetzt das Verhältnis zwischen dem N,
was der Input ist und dem R, was der Output ist.
Und in diesem Falle ist es tatsächlich eine Input-Output-Beziehung.
Denn wir wollen ja rechnen und dann wissen wir,
die Richtungsunabhängigkeit geht über den Jordan. Also wenn R gleich eins,
wenn N gleich Null ist, dann ist die Fakultät gleich eins.
Und in allen anderen Fällen muss sich die Fakultät berechnen,
indem ich die Fakultät von N-1-1-Berechner,
ein Zwischenergebnis bekommen für die Fakultät von N minus 1
und ich muss dieses Zwischenergebnis nochmal mit N multiplizieren,
um das Endergebnis R zu erhalten.
Alles, was ich gemacht habe, ist sozusagen das,
was hier implizit der Funktionswert ist,
ist jetzt hier immer ein R ist gleich.
Also hier sage ich eben, R ist gleich eins,
das steht hier explizit da. Hier sage ich,
das Resultat ist der Wert dieses Ausdruck,
also muss ich das Resultat aus einem Zwischenergebnis mit der Multiplikation
mit N erzeugen und dieses Zwischenergebnis berechne ich,
indem ich das Programm für N
minus eins aufrufe
So, und da können wir jetzt hingehen,
die Definition übertragen wir einfach in ein Prolog-Programm, die erste Zeile,
das steht praktisch schon da, die Fakultät von null ist eins.
Null als Input wert, eins als Output wert.
Im anderen Falle machen wir mal hier weiter, müssen wir
die Fakultät von N minus eins berechnen. Dazu brauchen wir erstmal ein N minus eins,
das wird hier berechnet, das ist praktisch die Konsumption,
die uns auch die Terminierung des ganzen Prädikates im Endeffekt sicherstellt,
dann können wir gegeben dieses N minus eins die Fakultät von N
minus eins berechnen Und anschließend müssen wir auf dieses Zwischenergebnis,
die Fakultät für N minus 1,
nochmal das N drauf multiplizieren, um das Ergebnis zu bekommen.
Und das Ergebnis ist eben die zweite Argumentposition, die wir hier haben.
Das ist nur eine Eins-zu-eins-Übertragung dieser Spezifikation in dieses Programm.
Was noch hinzukommt, ist dieser Gart hier,
der dafür sorgt, dass die zweite Klausel nur aufgerufen wird,
wird, werten die UN noch größer Null ist.
Das ist ganz einfach. Deshalb erforderlich,
weil ich in einem Umgebung, wo ich nach Alternativen frage,
die es ja nicht geben kann,
die Fakultät ist ja eindeutig gegeben,
wenn ich ein N habe, kenne ich die Fakultät eindeutig.
Da gibt es ja
keine alternativen Wertebindungen
Wenn ich diese Überwachung nicht mache, würde ich,
nachdem ich das erste Ergebnis vermittelt habe,
weiter das N und das Ergebnis ermittle ich ja hier am Rekursionsabschluss,
dann würde ich das enden, was in diesem Falle null ist,
wieder um eins verringern auf minus eins,
dann im nächsten Durchlauf auf minus zwei,
auf minus drei, auf minus vier.
In der Hoffnung, dass ich jemals wieder bei der Null vorbeikomme.
Die Hoffnung erfüllt sich natürlich nicht und deshalb kriege ich einen Terminierungsfehler,
wenn ich nicht dafür sorge,
dass die zweite Klausel nur dann aufgerufen wird,
wenn tatsächlich noch ein N größer als null vorliegt.
Denn nur dann macht es Sinn,
das End zu verkleinern in der Hoffnung,
dass die Null irgendwann
mal dabei entsteht
Gut, wir können uns das Rekursionsschema mal angucken.
Wir rufen das beispielsweise mit einer Vier auf.
Also dazu müssen wir sagen, dass das Prädikatsschema sagt ja,
dass diese natürliche Zahl unser Input ist und dass das das Resultat sein kann.
Also dies muss instanziiert sein mit einer natürlichen Zahl.
Hier kann es instanziert sein, es muss aber nicht instanziert sein.
Das ist zum ersten Mal tatsächlich ein Prädikat,
wo wir jetzt nicht mehr ein Prädikatsschema haben, wo wir beliebig instanzieren können.
Bisher hatten wir immer Prädikate, Wir haben mit Ausnahme des Find Alls,
das ist eben auch ein Nicht-beliebig-instanziierbares Prädikat, aber das war ja kein selbst definiertes.
Die Selbstdefinierten, die wir bisher geschrieben hatten,
waren immer von der Art, dass man beliebige Instanzierungsvarianten verwenden durfte.
Das geht eben jetzt hier nicht mehr. So,
also wir rufen das Ganze mit einer Vier auf und das
wird getestet mit dem Rekussionsabschluss. Der Rekussionsabschluss sagt,
bist du eine Null, das bin ich natürlich nicht.
Also muss der Rekussionsschritt aufgerufen werden. Es wird getestet,
ob das noch größer als Null ist.
Das ist der Fall. Also wird es um eins verringert,
auf drei und mit dieser drei wird das ganze Rekursiv
wieder aufgerufen
Wir haben hier nach wie vor wieder eine uninstanzierte Variable,
mit der wir, deren Wert wir ermitteln müssen.
Der nächste Auftrag ist, ja, um die Fakultät von 4
zu berechnen, müssen wir die Fakultät von 3 berechnen.
Und das wird auf die Fakultät von 2 zurückgeführt und
das wird auf die Fakultät von 1 zurückgeführt.
Bis wir am Regulsionsabschluss ankommen und jetzt einen Wert haben,
den wir in die Berechnung hineingeben können.
Und jetzt beim rekursiven Aufstieg passiert jetzt die Berechnung für auf
den einzelnen Rekursionsebenen, indem wir immer dieses Zwischenergebnis,
was wir jetzt haben,
mit dem N multiplizieren müssen
Und das Zwischenergebnis, was wir im letzten Schritt haben,
ist eben diese Eins, das müssen wir mit dem N
multiplizieren und da kriegen wir hier ein Eins heraus.
Wir müssen im nächsten Schritt wieder dieses Zwischenergebnis mit dem N,
das ist in dem Fall eine Zwei multiplizieren und es kommt seiner Zwei,
wir müssen es dann mit einer Drei multiplizieren,
es kommt zu der Sechs und dann müssen wir es mit der Vier multiplizieren und wir kriegen die 24 als Ergebnis heraus.
Wir sehen also hier im Abstieg,
passiert praktisch nur die Suche nach dem Rekursionsabschluss.
Wir verringern dieses gegebene Argument so lange,
bis wir irgendwann mal bei der Null ankommen.
Und die ganze Berechnung der Fakultät passiert hier auf dieser Argumentstelle
beim Rekursiven
Aufstieg wieder
Ja, nachdem wir über den rekursiven Aufruf ein Zwischenergebnis ermittelt haben,
dann können wir die entsprechende Multiplikation mit dem N machen,
damit die Fakultät tatsächlich berechnet werden kann.
Das wird auch als Induktion bezeichnet, weil wir hier vom
einfachsten Fall zum kompliziertesten Fall das Ergebnis sukzessive aufbauen.
Der rekursive Abstieg hier dient wirklich nur dem Zweck,
die diesen Induktionsanfang zu finden,
damit wir vom Induktionsanfang aus das sukzessive das Ergebnis berechnen können.
Das ist ein Spezialfall einer schlichten Rekursion, schlichter Rekursion heißt immer,
dass die Berechnung eigentlich nur auf einem Zwack zeigt, der Rekursion erfolgt.
Entweder beim Abstieg oder beim Aufstieg. Schlichte Rekursion heißt,
insbesondere, dass wir maximal einen Aufruf,
rekursiven Aufruf oder genau einen Rekursiven-Aufruf pro Klausel haben, das ist hier gegeben.
Wir haben also tatsächlich nur einmal,
dass wir dieses Fakultätsprädikat selbst wieder aufrufen und wir unterscheiden dann eben die aufsteigende Rekursion,
die wir eben gesehen haben und der andere Fall die Endrekursion oder auch absteigende Rekursion oder repetitive Rekursion oder Till Recursion genannt.
Das ist,
wenn die Berechnung nur beim rekursiven Abstieg erfolgt
Ja, bei der aufsteigenden Rekursion haben wir eben nur die Aufgabe,
um den Induktionsanfang oder Rekursionsabschluss zu finden und von dort aus beim rekursiven Aufstieg die Berechnung durchzuführen.
Die Fakultät war ein Beispiel. Und Verwendung ist dafür immer,
dass wir manchmal, nein, die Verwendung,
wir können es natürlich allgemein verwenden,
aber nicht entrekursive oder aufsteigende Rekursion erlaubt es uns in vielen Fällen,
richtungsunabhängige Implementierung zu machen, die aber typischerweise extrem ineffizient sind.
Weil sie, und wir werden das gleich sehen,
versuchen ein Ergebnis da zu ermitteln,
dass sie erstmal den Ergebnisse generieren in der Hoffnung,
dass das richtige Ergebnis irgendwann mal vorbeikommt.
Und das ist klar, dass im allgemeinen Fall ein solches Generate- und Testverfahren nicht effizient sein kann.
Endrikussion, die gesamte Berechnung erfolgt beim rekursiven Abstieg. Das Ergebnis liegt fest,
wenn wir unten am Rekursionsabschluss angekommen sind, das heißt,
wir brauchen nichts mehr weiter zu berechnen und der Rekursionsaufstieg sozusagen dient nur noch der Ergebnisübermittlung,
dann können wir das Ganze sehr effizient implementieren,
weil wir gar keine Zwischenergebnisse mehr brauchen.
Bei der Fakultätsberechnung hat man gesehen, wir brauchten immer die Zwischenergebnisse,
damit wir die hinterher wieder für die Berechnung verwenden können.
Die Ends waren das in diesem Falle. Und wenn wir
aber das Ergebnis bereits unten am Rekursionsabschluss vorliegen haben,
passiert keine Berechnung mehr.
Das heißt, der gesamte Verwaltungsaufwand für die Zwischenergebnisse entfällt.
Und diese Zwischenergebnisse werden auf einem Stack verwaltet und deshalb brauche
ich in diesem Falle überhaupt keinen Stack anzulegen.
Und das ist üblicherweise mit einem erheblichen Effizienzgewinn verbunden
Sodass wir also eine wichtige Frage in Zukunft immer sein wird,
können wir eine endrekusive Implementation angeben,
weil wir dann den Vorteil haben, dass das Ganze effizient ist.
Und das ist tatsächlich beeindruckend, die Unterschiede, die dann entstehen.
Wir müssen eine Endrekulision immer dann verwenden,
wenn wir den Induktionsanfang überhaupt nicht kennen.
Im Falle der Fakultät haben wir den Induktionsanfang,
gleich Rekursionsabschluss gekannt, dann wissen wir,
wenn N gleich Null ist,
ist die Fakultät gleich eins.
Dies kennen wir
Anderen Fällen ist das nicht bekannt. Wir werden uns den
größten gemeinsamen Teile angucken. Wenn wir wüssten, was der größte,
was der Rekursionsabschluss ist, würden wir den größten gemeinsamen Teiler kennen.
Wir müssen ihn aber erst berechnen. Das heißt,
wir wissen zwar, welche Eigenschaften der Rekursionsabschluss haben wird,
aber wir kennen ihn selber noch nicht. Wenn wir ein aufsteigendes,
also induktives Programm haben, dann können wir das immer in Absteigende,
in ein absteigendes Programm umwandeln, also in eine Endrikusive,
Variante umwandeln, indem wir dem Prädikat eine weitere Argumentstelle zur Verfügung stellen.
Diese Argumentstelle dient dann dazu, die Zwischenergebnisse zu sammeln und heißt deshalb auch Akkumulator,
weil dort sozusagen schon Teile des Ergebnisses immer aufgesammelt werden und gespeichert werden.
Und am Rekursionsabschluss wird nur noch das letzte,
die letzte Variablenbindung dieses Akkumulators als Ergebnis der Berechnung übergeben.
Und deshalb brauchen wir dafür eine gesonderte Argumentstelle,
damit wir dann am Rekursionsabschluss per Co-Referenz das Berechnungsergebnis nach außen kommunizieren können.
Also das wird eine Aktivität sein,
die uns im Weiteren immer wieder über den Weg laufen wird,
zu entscheiden, haben wir eine Endrekursive Definition, haben wir eine nicht-entrikosive Definition,
sind wir in der Lage, Und wenn ja,
wie natürlich die nicht entrinkursive Variante in eine endrekursive Variante zu übersetzen.
So, das wachen wir jetzt gleich mal. Hier
ist zur Erinnerung nochmal die induktive Variante, die wir gerade implementiert hatten.
Dadurch gekennzeichnet, dass sich erst den rekursiven Abstieg mache,
ein Zwischenergebnis ermittle und anschließend dieses Zwischenergebnis Weiterverarbeite, um das Endergebnis zu produzieren.
Das ist immer das Merkmal aufsteigender Rekursion
Während Endregussion jetzt eine zusätzliche Argumentschnelle hinzunimmt,
die ist hier mit A, wie Akkumulator bezeichnet.
Und auf dieser Stelle wird die Fakultät, schückt für Stück aufgesammelt.
Und zwar bereits beim rekursiven Abstieg. Wir können also sagen,
wenn ich unten angekommen bin,
dann ist das Endergebnis gleich dem aktuellen Wert des Akkumulators.
Also ich zähle das N herunter, bis ich bei null
angekommen bin. Und wenn ich bei null angekommen bin,
steht auf dem Akkumulator mein Ergebnis wert.
Und den kann ich per Co-Referenz
auf die Resultatsargumentstelle übergeben
In allen anderen Fällen rufe ich die Fakultät rekursiv auf,
übergebe ihr das N-1-1,
also berechne die Fakultät für ein N gleich N-1-1,
multipliziere das aktuelle N auf den aktuellen Wert des Akkumulators und Teile
gegebenenfalls mit,
was bei dieser Berechnung herausgekommen ist.
Das ist die dritte Argumentstelle
Und das Ganze muss ich natürlich aufrufen,
weil jetzt ich hier durch den Akkumulator eine dreistelliges Prädikat habe,
die Fakultät tatsächlich, aber nur eine zweistellige Relation ist.
Ich muss den Akkumulator geeignet initialisieren. Und das passiert hier
an dieser Stelle, dass ich sage,
der Akkumulator wird initial mit dem Wert 1 aufgerufen und anschließend
werden die jeweiligen Werte unserer Zählvariablen des Ends dort immer drauf multipliziert.
Das ist also in diesem Fall eine 1,
weil die 1 das neutrale Element der Multiplikation ist,
wenn nicht additiv, irgendetwas akkumulieren wollte,
wäre es eben die Null,
mit der ich da initialisieren muss
Und jetzt können wir wieder hingehen und diese Definition umsetzen in eine Prolog, in einem Prolog-Prädikat.
Wir haben Ad1, diesen Rapper hier außen drumherum,
wo das zweistellige Fakultät auf dieses Dreistellige mit der 1 als Initialwert abgebildet wird.
Das ist diese Zeile hier. Wir haben zwei Argumente,
das N und die Fakultät von N. Wir rufen unser Unterprogramm auf,
damit mit dem N und der Fakultät von N.
Und wir initialisieren die zusätzliche Argumentstelle mit 1. Und jetzt kommt diese rekursive Definition,
die eingebettete Wenn denn N gleich Null ist,
das ist die erste Argumentstelle, muss R gleich A sein.
Das ist die dritte Argumentstelle, das ist die zweite Argumentstelle.
Und das steht hier. Wenn N gleich Null ist,
muss die zweite Argumentstelle gleich der dritten Argumentstelle sein.
Durch diese Co-Referenz wird das Ergebnis am Rekursionsabschluss vom Akkumulator auf die Resultatsargumentstelle übergeben.
Das ist der Rekursionsabschluss und der Rekursionsschritt sagt eben hier auch wieder ein Guard,
nur für Werte größer Null, damit wir verhindern,
dass weil wir Inditure-Zahlen haben,
dass das Programm im negativen Bereich wieder nach einer Null sucht.
Das muss ausgeschlossen werden. Ansonsten ringern wir eben das N
wieder um eins, das ist dieser Teil der Definition.
Wir multiplizieren den Akkumulator mit dem N, dem aktuellen N,
up, sorry, mit dem aktuellen N-Wert, das ist dieser Teil.
Und damit rufen wir die Fakultät auf mit dem neuen Wert für N,
mit dem neuen Wert für den Akkumulator und wir kriegen ein Resultat für diese Aufruf.
Und dieses Resultat, das ist jetzt das Wesentliche,
wird einfach nur per Co-Referenz an die nach draußen mitgeteilt.
Da erfolgt keine Berechnung mehr, mit diesem Ergebnis hier wird nichts mehr gemacht,
das wird unverändert per Co-Referenz am der Signatur des des Prädikats nach draußen kommuniziert.
Wenn wir das also jetzt gegeneinander halten,
wir haben einerseits hier keinen Rapper außen drum rum,
wir müssen hier, haben hier eine zusätzliche Argumentstelle,
die nur eine interne Bedeutung hat, nämlich Zwischenergebnisse zu akkumulieren.
Deshalb haben wir hier außen drum rum nochmal einen Rapper,
wir können ihn schreiben, wir müssen es natürlich nicht machen.
Wir können auch das als Signatur benutzen,
wenn wir wissen, wofür das hier steht.
Damit wir die gleiche Signatur haben, schreiben wir einfach außen drum rum.
Wir haben hier immer ein dreistilles Prädikat, hier ein zweistelliges Prädikat,
weil wir hier eben diese zusätzliche Argumentposition den Akkumulator haben.
Ansonsten ist es genau das Gleiche, wenn die Fakultät null ist,
dann wird hier die Eins ausgegeben, hier wird gesagt,
dann nimm den aktuellen Akkumulator wert und geb ihn als Ergebnis aus.
Der Rekursionsschritt hat in beiden Fällen den Guard vorneweg,
um zu gucken, bin ich noch im positiven Bereich?
In beiden Fällen muss dieses N, unser Ziel,
variable, dekrementiert werden, verringert werden, um eins.
Und jetzt kommt der wesentliche Unterschied im Aufsteigen,
Trikotsiven Fall, rufe ich zuerst die Rekursion auf
Uwe Erst das Prädikat rekursiv auf, ehe ich anschließend die Berechnung mit
dem Zwischenergebnis durchführe. Und hier berechne ich erst das Zwischenergebnis,
teile es der Prozedur beim rekursiven Aufruf durch den Akkumulator mit.
Und als letztes Teilziel in der Klausel passiert dann der rekursive Aufruf.
Und das ist wie in der funktionalen Programmierung auch.
Das haben sie bei Frau Dresteller bestimmt auch gehört. Das ist ein formales Kriterium,
an dem ich Endrekursion erkennen kann, wenn denn der rekursive Aufruf,
das letzte Teilziel in der Klausel ist,
ansonsten der letzte Ausdruck in der in der,
im Körper der Funktion, dann ist es endrekursiv.
Andernfalls, wenn dort noch mit diesem rekursiven Ergebnis weitere Berechnungen
durchgeführt werden, dann ist es eben nicht entrikossiv.
Was wir hier geschafft haben, ist tatsächlich eine Umwandlung in eine Rekursiv, endrekursive Definition.
Und hier sind die beiden Prädikats, die beiden Rekursionsschemata nochmal im Vergleich.
Also zur Erinnerung bei der aufsteigenden Rekursion suche ich nur den
Rekursionsabschluss und baue bei der Gelegenheit immer gleich Zwischenergebnisse auf,
die sagen, ja, mein X, was ich berechnen soll,
ist ein X1 mal 4. Dummerweise kenne ich das X1 noch nicht.
Also mache ich den rekursiven Aufruf, um genau dieses X1 zu berechnen.
Danach weiß ich, dass X1 gleich X2 mal 3 sein wird.
Nur dummerweise kenne ich X2 noch nicht und ehe ich,
das ist eben jetzt die Beschränkung der funktionalen Auswertung,
ich kann diesen Wert nicht ermitteln,
ehe ich den Wert für X2 nicht kenne.
Wir werden sehen, bei nicht numerischen Berechnungen und bei Perano-Zahlen
haben wir das schon gesehen, da ist das kein Hindernis.
Dort bin ich tatsächlich in der Lage,
mit uninstanziierten Variablen reinzugehen und eine Co-Referenzkette aufzubauen,
aber in dem Augenblick,
wo ich eine funktionale Auswertung zwischendrin mache,
ist diese Möglichkeit verbaut
Das geht also weiter, bis ich weiß, dass X3 X4
mal 1 sein soll und jetzt kann ich tatsächlich rechnen.
Denn jetzt weiß ich, dass X4 gleich 1 ist,
das heißt, jetzt kann ich X3 ausrechnen und da ich X3 kenne,
kann ich jetzt X2 ausrechnen und da ich X2 kenne,
kann ich jetzt X1 ausrechnen und da ich X1 kenne,
kann ich jetzt X ausrechnen.
Das war der Aufsteigendrekursive Fall und jetzt der endrekursive Fall.
Ich zähle natürlich das N wieder nach unten,
bis ich beim Rekursionsabschluss angekommen bin,
aber ich führe die Berechnung der Fakultät hier bereits auf
dem Akkumulator durch
Initialisiert habe ich den Akkumulator mit eins. Danach wird eben
das aktuelle N drauf multipliziert und ich kriege vier,
danach wird das aktuelle N drauf multipliziert.
Ich bekomme die zwölf. Das aktuelle N wird,
die zwei wird drauf multipliziert, ich bekomme die 24 und
die Multiplikation mit dem aktuellen N liefert mir die 24.
Per Co-Referenz wird das Ergebnis auf die vierte Argumentstelle übertragen,
die dritte Argumentstelle übertragen. Und es ist auf der
dritten Argumentstelle beim Abstieg bereits wieder eine Co-Referenzkette aufgebaut worden.
Die sagt, X ist gleich XX1, gleich X2,
gleiche X3, gleich X4. Hallo,
da steht ja der Wert für
Das heißt, hier passiert keine Berechnung mehr. Wir sehen,
der Wert ist immer bereits bekannt, wenn ich hier unten angekommen bin.
Das heißt, dieser ganze Teil der Berechnung kann entfallen.
Der muss nicht mehr gemacht werden. Und ich muss nicht diese Zwischenergebnisse,
die ich hier brauche, in diesem Falle also das 1,
2, 3, 4, irgendwo zwischenspeichern.
Das heißt, die gesamte Stack-Verwaltung entfällt.
Und das macht die Sache so effizient.
Wir gucken uns drei weitere Beispiele an. Das eine ist
eine Umwandlung von PA-No-Zahlen
in Inditscher-Zahlen
Also positive Interacha-Zahlen natürlich, PA-No zahlen ja natürliche Zahlen sind.
Wir gucken uns an, wie wir den größten gemeinsamen Teiler
berechnen können und wir gucken uns ein Verfahren für die Multiplikationen
natürlicher Zahlen an, was ohne das kleine 1x1 auskommt.
Also eine sehr attraktive Art der Multiplikation. Erstes,
wie kriege ich den, oder wie kann ich mir PA-No-Zahlen generieren?
In der Übung gucken wir uns den umgekehrten Fall an,
wie kann ich für eine gegebene PA-No-Zahl ihren arathmetischen Wert ermitteln?
Jetzt, jetzt kombiniere ich praktisch die Welt der rekursiven Therme Nämlich S-Strukturen,
die ineinander eingebettet sind, mit der Welt der arathmetischen Auswertung.
Ich möchte ja genau diesen Sprung machen von der logischen,
sauberen Welt der PANO-Zahlen als eingebettete S-Strukturen hin zu der,
aus logischer Sicht unsauberen Welt Theatrithmetik,
die allerdings effizient dazu implementieren geht.
Gut, was muss ich machen? Ich weiß,
dass die, wenn ich nur eine Null folge,
also das erste Argument ist immer die Dezimalzahl,
das zweite Argument ist die PANO-Zahl
Wenn ich eine PANO-Zahl für arrethmetisch null erzeugen will,
dann ist das auch PANO-Zahl null. Das war ja
unser Abschlusselement, was wir in die PANO-Zahl eingefügt haben.
Es kann jedes beliebige andere sein, wir haben irgendeins genommen.
Ansonsten muss ich eben mein N wieder um eins verringern.
In der Hoffnung, dass sich das Gegebene N,
also beispielsweise drei, schrittweise durch Konsumptionen auf diese Null zurückführen kann.
Also ich subtrahiere wieder in jedem Schritt ein N-1 und erzeuge dafür eine Äquivalente Peano-Struktur.
Das passiert auf der zweiten Argumentstelle. Also das N wird um eins verringert,
dass hier wird eine gleichzeitig parallel dazu eine Peano-Struktur aufgebaut.
Wenn ich also so etwas habe, wie,
gib mir die Peano-Zahl von vier, dann weiß ich zu diesem Zeitpunkt bereits,
es wird ein S von irgendetwas sein.
Nur was innen drin steckt, das P, das kenne
ich noch nicht. Und um das P zu berechnen,
muss ich die Peano-Zahl von drei brechnen,
denn die muss dort hinein unifiziert werden,
damit die Peano-Zahl von vier nach außen mitgeteilt wird.
Also hier wird konsumiert, hier wird eine S-Struktur
aufgebaut durch Unifikation
Und natürlich wieder dieser Gart, der dient wieder dem gleichen Zweck,
ich muss verhindern, dass Ergebnisse im negativen Bereich der Integer-Zeilen gesucht werden.
Denn da sind keine. So, das Rekursionsschema hat man also,
ich rufe es mit vier auf. Ich weiß bereits beim Aufruf,
dieses Ziels, dass das XS von X1 sein wird,
dass X1 wieder eine uninstanzierte Variable, deren Wert ich noch nicht kenne.
Dann reduziere ich das N um eins und ich habe,
um das X1 zu berechnen. Ich weiß in diesem Augenblick
bereits, dass mein X1-Gleis S von X2 sein wird.
Das heißt, ich weiß jetzt bereits im Abstieg,
dass Mein Gesuchtes X, ein S von S von X2 sein wird.
Also es wird mindestens zwei Essschalen haben, möglicherweise mehr.
Aber das weiß ich noch nicht. Um das zu ermitteln,
rufe ich das Ganze mit N gleich 2 auf und kriege jetzt wieder eine Unifikationsgleichung,
die mir sagt, ja, dieses X2,
was ich hier suche, ist ein S von einem X3.
Zu diesem Zeitpunkt weiß ich bereits, dass das X
ein S von S von S von irgendetwas sein wird.
Das geht so weiter, bis ich hier unten dann weiß,
dass das X4 mal eine Null sein wird.
Und in diesem Augenblick, wo ich hier unten angekommen bin,
weiß ich bereits,
mein Ergebnis lautet S von S von S von S
von Null
Das steht hier fest, obwohl es hier so aussieht,
als ob hier ein Ergebnis übergeben wird,
ist es nicht der Fall.
Es ist wieder der gleiche Trick des guten Zauberers,
der den Trick längst gemacht hat, eh das Publikum es vermutet.
Dieses Ergebnis stand von vornherein fest, der äußere Teil.
Im zweiten Schritt stand dieser bis dorthin fest,
im dritten Schritt bis dorthin und im vierten Schritt
ist nur oder im letzten Schritt nur diese Null hier hinein unifiziert worden.
Alles andere stand hier unten bereits fest
Das heißt, auch hier ist es wieder so,
wir können den rekusiven Aufstieg entfallen lassen.
Das Ganze ist Entrekusiv. Wir gucken uns die Definitionen nochmal an.
Es ist nicht verwunderlich, der rekusive Aufruf ist das letzte Teilziel in der Klausel.
Und das ist eben unser formales Kriterium für Entrekusivität.
Das heißt, wir haben hier von vornherein eine Entrekusive Variante definiert.
Wir können das Ganze natürlich auch wieder nicht end-trikotsiv definieren.
Die Alternative steht immer zur Verfügung. Wir gucken mal,
ob wir auf die Art und Weise,
wenn ich vielleicht eine richtungsunabhängige Definition hinbekommen, weil ja hier Tatsächlich,
das haben wir jetzt noch nicht nachgewiesen,
aber das können wir uns ganz leicht überlegen.
Diese Definitionen, die wir hier haben, ist so,
wie wir es ihr angegeben haben, nicht richtungsunabhängig.
Wenn ich also diese Zahl hier uninstanziert lasse,
kracht es sofort bei der Überwachung dieses Guards.
In dem Augenblick will er einen Wert von N haben,
den kriegt er nicht, und damit beendet das Ganze mit Fehler.
Das heißt also, hier eine uninstanzierte Variable reinzuschreiben, das geht nicht.
Die Hoffnung ist jetzt, dass wir über die Nicht-Entrikussive Variante
eine richtungsunabhängige Definition bekommen. Und wir wissen,
dass Richtung und das Entrikussion und Nicht-Entrikussion sich nur darin unterscheiden,
den Aufruf mache, bevor eine Berechnung passiert.
Also ich muss die Berechnung, den Berechnungsschritt hier vertauschen mit dem
rekursiven Aufruf und dann habe ich im Prinzip eine nicht-entrikosive Variante.
Ich lasse den Guard auch noch weg, um zu schauen,
was passiert. Denn wenn der Guard hier drinstehen würde,
würde das Gleiche passieren wie bei der anderen Definition,
dass wir da ein Instanziierungsproblem bekommen.
Also wir haben als Wunsch tatsächlich die Instanziierungsvariante,
wir haben den Wunsch, dass alle Instanziierungsvarianten möglich sein sollten.
Deshalb also hier beliebig instanziierbar und hier auch beliebig instanziierbar.
Gut, dann gucken wir uns an, was passiert.
Wir rufen das Ganze also uninstanziiert auf und wir kriegen tatsächlich
einen Wert für N, nämlich zwei. Allerdings,
wenn wir das Ganze umgekehrt aufrufen mit dem zweiten Argument und instanziert,
kriegen wir auch das gewünschte Ergebnis,
dass die zwei PR-Notzahl S von S von 0 ist.
Aber wenn wir hier nach Alternativen fragen, dann kriegen wir
wieder ein Terminierungsproblem. Und das ist das gleiche Terminierungsproblem,
was wir schon in anderer Richtung kannten, wo wir das suchen,
ins Negative verhindern mussten, was hier passiert ist.
Er hat eine Zahl, der Witz ist ja,
dass ich jetzt hier das N immer erhöhen muss.
Ich fange ja an Ergebnisse zu generieren,
beginnt mit der Null und schaue immer,
ich vergrößere die Zilbayable immer,
bis ich am korrekten Ergebnis vorbeikomme.
Und das korrekte Ergebnis ist dieses hier
In diesem Augenblick habe ich das korrekte Ergebnis für N gleich 2 ausgegeben.
Und jetzt erhöhe ich das N auf 3,
auf 4, auf 5, auf 6, auf 7,
in der Hoffnung, dass ich jemals wieder bei der Null vorbeikomme.
Und das passiert nicht. Das ist genau der Terminierungsfehler,
das Terminierungsproblem hier, nach dem ersten Ergebnis wird es kein zweites Ergebnis geben.
Und wir können das auch wieder durch Cut oder letztens
hatte ich es Once genannt, indem wir das einbetten und sagen,
wir wollen nur ein Ergebnis haben.
Aber das ist dann wirklich wieder eine radikale Lösung,
die uns in jedem Fall das Ergebnis auf eine Instanzierungsvariante
auf ein Ergebnis beschränkt
Also auch, wenn wir das dann vollständig unterspezifiziert aufrufen kriegen,
wenn ich mehrere Ergebnisse aufgezählt.
Bei vollständiger Unterspezifikation müssten wir ja jetzt alle Paare aufgezählt bekommen.
Für PANO-Zahlen und arithmetische Zahlen. Also die Null ist die Null,
die Eins ist die S von Null,
die Zwei ist die S von S von Null und so weiter.
Das passiert natürlich nicht. In dem Augenblick, wo ich Alternativen abschneide,
dann passiert das radikal und dann kriege ich eben nur noch
das erste Ergebnis und alle weiteren nicht mehr.
Also, ja, wir können hier tatsächlich in gewisser Weise eine Richtungsunabhängigkeit herstellen,
aber es bleibt immer noch eine Hintertür wo ein Terminierungsproblem uns verfolgt.
Die Rekursion Schemata, nochmal hier für den Aufruf mit dem ersten Argument unterspezifiziert,
also gib mir die Intager-Zahl, die der PA nur 3 entspricht.
Wir sehen wieder, dass hier wegen der Berechnung nach dem Rekursiven,
nachdem Rekursiven Aufruf eine Co-Referenzkette aufgebaut wird, die sagt,
ja, das X wird irgendein X1 sein plus 1,
aber ich muss erstmal diese PA-Notzahl bis zur Null abbauen,
damit ich weiß, was mein Anfangswert für die Berechnung ist.
Und genau das passiert dir, ich baue das Die S-Strukturen so lange ab,
bis ich nur noch die Null habe, dann weiß ich,
dass hier X3 gleich Null ist, dann weiß ich,
kenne ich X2, dann kenne ich X1, dann kenne ich X.
Das ist das, die Berechnungsvariante in diesem Aufrufschämen,
mit dieser Instanzierungsvariante und wenn ich das zweite Argument uninstanziert lasse,
dann fange ich dort eben an, S-Schein aufzubauen.
Also ich reduziere, und das ist jetzt das Wesentliche,
ich kann ja, alles, was ich weiß,
ist, dass die drei ein N1 plus 1 sein soll,
aber das kann ich eben nicht berechnen.
Hier ist eine uninstanzierte Variable drin. Die kann ich eben
erst berechnen, nachdem ich den rekursiven Abstieg gemacht habe.
Und der rekursive Abstieg ist jetzt ein vollkommen uninstanzierter Aufruf.
Hier, dieser SX habe ich noch nie gekannt.
Alles, was ich jetzt weiß, ist,
dass ein X1 hier das Innere dieses X-Wertes sein muss.
Aber was der Wert von X1 ist, das weiß ich
noch nicht. Und was der Wert von N1 ist,
das weiß ich auch nicht, weil ich nicht in der Lage bin,
die Umkehroperation dieser Addition durchzuführen.
Das ist eben nicht relational. Und deshalb rufe ich das
jetzt mehrfach uninstanziert auf, bis ich irgendwann mal durch Zufall
bei der PA00 vorbeikomme
Ja, und ich von der PA und nein, nicht durch Zufall.
Ich komme immer bei der PA und 0 vorbei,
aber die PA und 0, die ich jetzt hier gefunden habe,
nachdem ich dreimal einen rekursiven Aufruf gemacht habe,
die erlaubt es mir eben,
die gewünschte PA-No-Zahl für 3 zu rekonstruieren.
Ja, das ist also sozusagen blind links in den Suchraum hineingegangen.
In der Hoffnung, ich komme irgendwann mal an einer Kombination von
arathmetischem Wert und PA-No-Zahl vorbei, der die Bedingungen des Programms erfüllt.
Also ich generiere Varianten und prüfe immer,
ob die Varianten miteinander konsistent sind.
Also ob der arathmetische Wert, den ich dort habe,
mit dem PA-No-Wert übereinstimmt
Deshalb ein Generate- und Testverfahren, dass das eben ohne,
ohne jetzt tatsächlich die Werte weiter zu bearbeiten,
blind links das Programm aufruft, in der Hoffnung,
dass das Ergebnis herauskommt, was genau diese Bedingungen erfüllt,
dass nämlich das N1 hier plus 1 gleich 3 ist und dass dieses X eine S-S-Struktur ist,
mit dem Inneren,
die durch diese Kur-Referenzkette,
mit dem Inneren,
das durch diese Choreferenzkette festgelegt worden ist
Gut, soviel zur Umwandlung von PA-No-Zahlen. Auch hier wieder,
PA-No-Zahlen sind unsere einfachste, rekursive Struktur, die wir haben.
Deshalb also das an diesem Beispiel. Wir werden uns in
der Übung auch hier in der Vorlesung bei den Listen
dann die Analogie zu den PA-No-Zahlen zunutze machen,
um zu überlegen, wie können wir von den Prädikaten,
die wir für die PA-No-Zahlen kennen, äquivalente Programme ableiten für die Listenverarbeitung.
Also was würde dann dem, ja nehmen wir mal,
dem PA-No-To-Intager in der Welt der Listenverarbeitung entsprechen
Ja. Ach so, ja, gut,
dann stellen Sie erst mal Ihre Frage. Ich kann meine
hinterher noch mal wiederholen. Wie das überhaupt ist beim Primikat
, hat das der Fall gesagt, umwandelt und unspezifiziert?
Ja, das haben wir doch schon gesehen. Bei der
Piano-Addition ging das. Aber ich meine jetzt mal um,
Sascha ziehen. Nein, es gibt keine allgemeingültige Lösung dafür.
Was ich machen kann, ist immer, es sind ja zweimal Funktionale.
Einmal von PA, nur nach Integer, einmal von Integer
nach Beano. Ich kann die vajablen Instanzierung überwachen und dann
die verschiedenen Wege getrennt implementieren. Das kann ich immer machen,
der Weg ist immer offen, aber auf der Basis der
reinen Logikprogrammierung nein
Gut, also nochmal die Frage, ich gebe davon aus,
ich habe so etwas wie die PA nur zu Indicer für Beano-Zahlen gemacht,
was mir sagt, was ist der arrithmetische Wert einer PA eine Struktur,
wenn ich jetzt Listen habe,
was würde dem dann entsprechen in der Welt der Listenverarbeitung?
Ich stelle die Frage nachher nochmal. Wir kommen zu
dem Punkt noch. Okay, also größter gemeinsamer Tyler,
das Berechnungsverfahren sollte ihn aus dem Schulunterricht vielleicht noch dunkel in Erinnerung sein.
Wir kennen das Funktional natürlich immer,
wenn ich den größten gemeinsamen Tyler 2er zahlen wissen will,
dann Ist das X in dem Augenblick, wo X gleich Y ist?
Also der größte gemeinsame Teil ab von 4 und 4 ist eben 4.
Von 8 und 8, das ist 8. Wenn die Zahlen
ungleich sind, dann sortiere sie nach Größe,
sodass der große, die große Zahl immer vorne steht,
die kleine Zahl hinten steht.
Das macht die zweite Zeile. Wenn also X kleiner ist
als Y,
dann nimm Y nach vorne
Weil die große Zahl immer vorne stehen soll.
Es hat einfach nur den Grund,
weil ich jetzt hier das eine von dem anderen subtrahieren möchte.
Und da muss die große, die kleine Zahl von der Großen abgezogen werden.
Ansonsten kriege ich negative Werte, das will ich nicht. Gut,
also hier werden nur die Argumente sortiert, nach Größe,
danach steht das große Argument immer vorn, das Kleine hinten.
Und weil ich das jetzt habe, kann ich das Kleine
vom Großen subtrahieren und mit der Differenz wieder aufrufen.
Also ich rufe das Ganze rekursiv auf,
indem ich hier die Differenz von der beiden Zahlen
gebildet habe und hier die zweite Zahl belasse
Und das mache ich rekursiv immer weiter,
solange bis ich bei der Bedingung X gleich Y angekommen bin.
Das ist der Trick des größten Gemeinsamen Tylers, ermitteln,
charakteristisch ist hier erst am Rekursionsabschluss, kriege ich das gewünschte Berechnungsergebnis.
Das heißt, ich weiß nur, der Rekursionsabschluss muss diese Bedingung erfüllen.
Ich weiß aber nicht, wo er ist. Das kann ich
nicht sagen. Und deshalb kann ich eine induktive Berechnung auch nicht machen,
weil ich nicht vom Rekursionsabschluss eine Berechnung durchführen kann,
sondern ich muss tatsächlich eine Endrekursive Berechnung durchführen.
Wir übertragen jetzt also diese Definition in unsere relationale Welt.
Wir brauchen wieder ein zusätzliche Argumentposition für das Ergebnis.
Das heißt hier G, wie größter gemeinsamer Teiler. Wenn denn X gleich Y ist,
dann ist das gewünschte Ergebnis eins von beiden, entweder X oder Y,
dann können wir leben, was immer wir wollen.
Wenn denn X kleiner ist als Y,
dann müssen wir die ersten beiden Argumente in der Reihenfolge vertauschen.
In allen anderen Fällen bilden wir die Differenz aus dem größeren und dem kleineren,
Wir wissen ja jetzt, das Größere steht vorn,
das hat ja der zweite Fall uns sichergestellt.
Wir bilden die Differenz und wir rufen den größten gemeinsamen Teiler mit
der Differenz und dem zweiten Argument auf und kriegen unser gewünschten größten gemeinsamen Teiler.
Jetzt haben wir eine relationale Definition und die übertragen wir einfach wieder in unser Prolog-Programm.
Wenn X gleich Y oder mit anderen Worten,
wenn erste Argumentstelle gleich zweite Argumentstelle, dann auch gleich dritte Argumentstelle.
Das ist unser erster Fakt hier. Unser Rekursionsabschluss und jetzt
sehen wir hier, dass Fakten durchaus nicht immer Grundtarme sein müssen.
Die müssen nicht vollständig instanziert sein. Wir können hier auch
vollkommen uninstanzierte Variablen drin haben, die einfach nur sicherstellen,
alle drei müssen übereinstimmen.
Mehr sagt das Ding ja nicht. Und das ist ein Fakt,
wie, was weiß ich, der Fakt, dass Hans-Susi liebt.
Da ist kein Unterschied dazwischen. Nur die Stelligkeit. Zweite Klausel ist,
wenn, denn das X kleiner ist als das Y,
dann vertausche sie, ruf sie in umgekehrter Reihenfolge auf mit
Y vorn und X als zweites Argument.
Im umgekehrten Fall X ist größer als Y, also das ist ja klar,
Den Gleichheitsfall haben wir ja hier oben bereits abgedeckt.
Den müssen wir noch nicht mehr extra betrachten. Das ist also
das Kompliment zu diesen beiden Fällen. X ist größer als Y,
dann bildet die Differenz aus X und Y und rufe den
größten gemeinsamen Teiler mit dieser Differenz und dem unveränderten zweiten Argument
auf und du kriegst das gewünschte Ergebnis den größten gemeinsamen Teiler.
Und das können wir testen, wir können jetzt schauen,
der größte gemeinsame Teiler von 15 und 9 ist 3.
Das hört sich plausibel an, von 2 und 3
ist es eins,
weil es
beides Primzahlen sind
Wenden wir uns das Rekursionsschema angucken, dann sehen wir eben,
wir rufen das Ganze mit 15 und 9 auf.
Wir bauen hier wieder auf der zweiten Argumentposition eine Co-Referenzkette auf,
die uns sagt, dass das X gleich einem X1 sein wird.
Nur das X-1 kennen wir noch nicht. Mit anderen Worten,
um zu berechnen, was der größte gemeinsame Teil hat
von 15 und 9 ist, müssen wir berechnen,
was der größte gemeinsame Teiler von 6 und 9 ist.
Und die 6 hier ist eben gerade die Differenz zwischen
15 und 9. Jetzt steht hier die kleinere Zahl vorne,
das ist nicht gut, also müssen Sie Ihre Argumentposition vertauschen.
Die große Zahl ist wieder vorne
Ich rufe das Ganze also wieder auf, um zu wissen,
was der größte gemeinsame Teiler von 9 und 6 ist,
muss ich die Differenz zwischen 9 und 6 bilden,
das ist die 3 und die 6 bleibt unverändert,
ich muss den größten gemeinsamen Teiler von 3 und 6 bestimmen.
Wieder steht die kleine Zahl vorne, was nicht gut ist,
die große Zahl muss nach vorne und erneut das Gleiche nochmal,
um diesen größten gemeinsamen Teiler zu ermitteln,
muss ich die Differenz und mit dem 3 überprüfen,
also 6 minus 3 ist 3 und die 3 bleibt unverändert.
Jetzt ist die erste Argumentstelle gleich der zweiten Argumentstelle,
dann kenne ich auch
den größten gemeinsamen Teiler
Das heißt,
auf beim rekursiven Aufstieg passiert wieder überhaupt keine Berechnung mehr,
denn diese Co-Referenzkette auf der dritten Argumentposition hat garantiert mir das in dem Augenblick,
wo ich hier unten den Wert für X5 kenne,
hier oben der Wert für X bekannt ist.
Und den will ich wissen, das ist mein Berechnungsergebnis.
Das heißt, das Ganze ist wieder entrekursiv und
wir schauen uns das Prädikats, die Prädikatsdefinition an.
Tatsächlich in allen Klauseln gilt,
dass der rekursive Aufruf das letzte Teilziel der Klausel ist und
damit ist das ganze Endrekursiv
Letztes ist die russische Bauernmultiplikation unter sehr vielen Namen bekannt,
so was wie Abissynisches Rechnen oder ägyptische Multiplikation.
Das mag irgendjemanden irgendwie despektierlich erscheinen, dass hier
der russische Bauer irgendwie in ein schlechtes Licht gerückt wird.
Tatsächlich ist aber die Art und Weise hier zu multiplizieren eine überraschend einfache Art zu multiplizieren,
die schon mit einer gewissen Bauernschleue gleichzusetzen ist.
Daher wahrscheinlich auch der Name. Kennt jemand
das Verfahren
Einer zumindest. Das ist, zwei, drei, ja okay,
gut, aber dann lohnt es sich es nochmal zu erklären.
Der Trick besteht hier also darin, dass ich kein kleines 1x1 brauche.
Das, was in der Schule typischerweise gepaukt wird,
das ist hier unnötig. Die Operationen, die ich brauche,
sind einzig und allein, verdopple eine Zahl, halbiere eine Zahl.
Subtrahiere 1 von einer Zahl und addiere.
Das sind die einzelnen Operationen, die ich kennen muss.
An keiner Stelle brauche ich das kleine 1x1, nur mit der 2.
Das brauche ich, ich muss verdoppeln und halbieren können.
Das anders geht es nicht
Das Ganze ist besonders interessant, aus der Sicht des Informatikers,
weil halbieren und verdoppeln von Binärzahlen natürlich eine ganz,
ganz einfache Bit-Operation ist.
Das ist nichts anderes als ein Bitchift nach links oder nach rechts.
Und deshalb ist das also ganz witzig von der,
vom Grundprinzip her. Das Schema ist relativ einfach,
wenn ich denn, ich soll zwei Zahlen miteinander vergleichen,
multiplizieren, also ein Faktor 1, ein Faktor 2.
Und falls der Faktor 2 eine 1 ist,
dann ist das Ergebnis der Faktor
Also irgendetwas mit eins multipliziert, ist eben irgendetwas.
Wenn das F2 ungerat zahlig ist, dann heißt das hier,
mach es geratzahlig. Dann ziehe einfach eins davon ab,
mache F2 geratzahlig und weil du dann natürlich einen Fehler machst,
du kannst ja nicht,
man kann ja nicht einfach einen Faktor um eins reduzieren,
da entsteht ja ein Fehler,
muss ich also einmal das F1 nochmal drauf addieren,
um diesen Fehler zu kompensieren.
Ich mache hier den einen Faktor um eins kleiner,
also muss ich den anderen Faktor nochmal drauf addieren,
damit es wieder das Gleichgewicht gewahrt ist.
Und diesen zweiten Schritt hier habe ich nur deshalb gemacht,
damit ich sicherstellen kann,
dass der zweite Dass das zweite Argument immer geratzahlig ist,
wenn es ungeratzahlig ist, dann mach es zwangsweise geratzahlig.
Wenn es geratzahlig ist, dann kannst du es durch zwei
teilen und auch damit macht man ja wieder einen Fehler.
Man kann ja nicht einfach einen der Faktoren am Zweit, auf die Hälfte verringern.
Deshalb muss man den anderen Faktor mit zwei multiplizieren.
Und dann ist das Gleichgewicht wieder gewahrt. So,
jetzt haben wir hier einen Fall vorliegen,
wo wir Berechnungen sowohl im rekursiven Abstieg machen müssen, also hier,
wo wir das Ganze mit einem arithmetischen Therm hier multiplizieren,
hier dividieren, aufrufen,
als auch beim rekursiven Aufstieg,
wo wir eben hier diese Subtraktion vornehmen müssen
Das heißt, in diesem Falle,
schlicht Rekursion ist natürlich Quatsch, Endrekursion ist hier nicht möglich.
Wir übertragen das Ganze wieder in eine relationale Definition,
indem wir eine zusätzliche Argumentstelle für das Produkt hinzufügen und
das Ganze jetzt ausdrücken im Begriffen einer Relation.
Also wenn der Faktor A2 gleich eins ist,
dann ist unser Produkt gleich dem ersten Faktor.
Falls der zweite Faktor Ungerat zahlig ist,
dann subtrahieren wir eins von ihm und machen ihn damit geratzahlig.
Und mit diesem geratzahlig gemachten zweiten Faktor rufen wir
das Ganze rekursiv auf
Und müssen anschließend nur das Zwischenergebnis, was wir dabei erzielen,
wieder korrigieren, indem wir den ersten Faktor nochmal drauf addieren.
Damit die Welt wieder in Ordnung ist. Ansonsten,
multiplizieren wir den ersten Faktor mit zwei,
wir dividieren den zweiten Faktor durch zwei und mit den so korrigierten Faktoren
rufen wir das Ganze rekursiv wieder auf und kriegen das gewünschte Produkt.
Das ist das entsprechende Programm dazu. Wenn der zweite Faktor
eins ist, dann ist das Ergebnis gleich dem ersten Faktor.
Wenn das Ergebnis ungereicht, wenn der zweite Faktor ungeratszahlig ist,
dann mache ihn geratzahlig
Und rufe mit dem geratzeitig gemachten zweiten Faktor das Ganze rekursiv auf und korrigiere anschließend das Ergebnis,
in dem der erste Faktor nochmal drauf addiert wird.
Im dritten Fall, wenn es geratzeitig ist,
dann verdopple den ersten Faktor, halbiere den zweiten Faktor,
rufe mit den so korrigierten Faktoren das Ganze rekursiv auf und du kriegst das entsprechende Ergebnis.
Wir testen, ach so, halt, was wir natürlich brauchen,
ist, jetzt ein Prädikat, was uns für arrithmetische,
also für normale Integrat-Zahlen, Grad und Ungeratzeitigkeit berechnet.
Wir hatten das ja schon mal für die PA-No-Zahlen gemacht,
jetzt müssen wir es für die Für die Intertat-Zahlen auch noch machen.
Und dazu benutzen wir in diesem Falle mal eine andere Definition,
die das Ganze eben auf die Modulo-Rechnung zurückführt.
Also wenn der Rest bei der Vision durch 2-1 ist,
dann ist es offensichtlich eine ungerade Zahl,
wenn der Rest bei Division durch 2-0 ist,
dann ist es eine gerade Zahl.
Das können wir testen und das sind durchaus die Ergebnisse,
die wir warten würden. Das können wir dann also einbauen
und wir können jetzt das Ganze die Bauernmultiplikation testen.
Vier mit sieben multipliziert ist 28,
24 plus 157 müssen wir jetzt glauben, es ist
3768. Wir müssen Testgrenzfälle testen
Ja, also eins mit eins multipliziert ist eins,
null mit eins multipliziert ist null.
Eins mit null multipliziert ist Falls, boah, woran liegt es?
Ja, so als eins, zwei bestimmt, dann wirst du erst antworten.
Das war der Arbeitungswert, das hier war das Buch.
Ja, genau. Hier steht die eins hart drin kodiert,
hier testen wir, ob es größer als eins ist.
Es kann also nie mit dem zweiten Faktor null aufgerufen werden.
Da kann man dann einfach noch den zweiten Abschluss lösen
Genau. Es ist extrem einfach zu korrigieren. Ich brauche nur
einen zweiten Fakt einzufügen, der die null Multiplikation erledigt.
Muss nur dann aufpassen, dass ich nicht möglicherweise doppelt,
nee, kann ich eigentlich nicht.
Wenn die null hart da drinsteht, können auch keine Doppelergebnisse auftreten.
Gut, also das ist einfach zu korrigieren.
Da haben wir überhaupt keine Schwierigkeiten. Wir können es
auch mal testen, ob Zusicherungen vielleicht verletzt werden können.
Also eine Zusicherung war ja, wir reden nur über natürliche Zahlen.
Klappt es auch für Integer-Zahlen? Also wir nehmen mal das
erste minus fünf hier an und wir kriegen tatsächlich das richtige Ergebnis,
eine negative,
ein negatives Produkt
Wenn wir es umgekehrt machen, kriegen wir das gleiche Problem,
was wir auch jetzt schon hatten,
mit negativen Zahlen können wir nicht multiplizieren.
Selbst wenn wir die Definition jetzt auf null erweitern würden,
ist es immer nur für positive Zahlen, für nicht negative Intage-Zahlen definiert.
Und das ist auch nicht so einfach zu reparieren.
Da müsste man eine separate Vorzeichenhandlung einführen. Gut,
wir können auch andere Instanzierungsvarianten testen, also was passiert,
wenn ich einen Faktor nicht kenne,
dann passiert genau das, was ich erwarten würde.
Es gibt ein Instanzierungsfehler,
wo kracht es
Im Zweifelsfall in der Wand ist. Vorsichtig,
wenn ich den ersten Faktor unterspezifiziert lasse, wo es hier
ein ist im ersten Fakt, mit dem ersten Faktor?
Hier unten ist es, ja. Für den zweiten Faktor
kracht sie oben bereits bei diesem größten Vergleich,
der darf nicht unterspezifiziert werden, für den ersten passiert es hier.
Allerdings schon eine Nummer eher, es passiert bereits hier in
dem Ottest
In dem Augenblick, wo ich in dem Ort-Test bin,
muss ich hier einen Größenvergleich machen, das ist das,
was ein instanziertes X erwartet.
Und hier wird kein instanzierter Wert geliefert. Das Gleiche eben
für den zweiten Faktor, das haben wir eben gesehen,
es ist auch der Größenvergleich, der uns das Ergebnis da verhindert.
Wir gucken uns das Rekursion, schämen wir nochmal an.
Wir wollen also vier mit sieben multiplizieren. Die zweite Zahl
ist ungerade, also müssen wir sie geratzahlig machen.
Und weil wir sie geratzeitig machen müssen, müssen wir nochmal die vier,
die wir eben unter den Tisch fallen lassen haben,
wieder drauf addieren,
damit das Ergebnis stimmt
Und jetzt ist der zweite geradzahlig.
Wir können den ersten Faktor verdoppeln, den zweiten halbieren.
Dann wird nur Cho-Referenz zwischen X1 und X2 hergestellt,
denn wir haben ja nichts falsch gemacht.
Wir müssen nichts weiter berechnen. Wir haben jetzt wieder hier
eine ungerade Zahl. Wir machen sie zwangsweise geradzahlig und müssen
dabei als Korrektur nochmal die Acht addieren.
Und kommen dann hier auf die 16 und die Eins.
Und das ist unsere Abbruchbedingung. Wir haben das Ganze auf
die Eins reduziert. Das heißt, wir können jetzt mit
der 16 plus 8 plus 4 das gewünschte Ergebnis konstruieren.
Das Ganze ist nicht endrekursiv, wir können hier nicht den Rekursionsaufstieg wegfallen lassen,
weil im Rekursionsaufstieg noch diese Korrekturberechnung hier durchgeführt werden müssen.
Und die können wir nicht vorher machen, die können wir erst machen in dem Augenblick,
wo diese 6-1 bekannt ist und das X-3 hier. Fürs Testen heißt das,
wir müssen wieder nochmal weitere Dinge mit uns angucken, also was hinzukommt,
sind jetzt noch so etwas wie Verletzung von Zusicherung,
denn wir haben ja jetzt erstmalig harte Zusicherungen gemacht und wichtige Spezial- und Grenzfälle,
also die Multiplikation mit der 1,
die Multiplikation mit der Null,
das sind wichtige Spezial- und Grenzfälle für die Multiplikation
Gut, soviel zur Art Metik. Die werden wir an
verschiedenen Stellen brauchen. Das Kernthema, was wir hier haben,
ist aber die Arbeit mit rekursiven Datenstrukturen.
Und wir hatten ja bereits PANO-Zahlen als einfachste rekursive Datenstruktur.
Das erweitern wir jetzt ein kleines bisschen um auf eine
zweistellige Struktur und eine zweistellige Struktur ist genau das,
was uns zu den verketteten Listen bringt.
Verkettete Listen sind immer in der internen Implementation zweistellige Strukturen.
Wir gucken uns das gleich an. Wir werden dann den Begriff
der Listenunifikation einführen müssen. Wir kriegen eine spezielle Listennotation,
die ist auch wieder, wie die Operatorin, nur syntaktischer Zucker.
Sie ändern nichts am Verarbeitungsmodell, sondern sie lassen nur eine
besonders bequeme Notation für Listen zu. Und für diese bequeme
Notation von Listen müssen wir uns dann entsprechend auch die Unifikation erweitern.
Wir schauen uns dann Beispiele aus dem Bereich Suchen und Sortieren an
und das sind einfache Beispiele und dann zwei komplexere Beispiele aus
dem Bereich Memoisierung bei der Suche und Suchraumverwaltung wenn es darum geht,
Alternative Suchstrategien zu implementieren,
die also nicht mit der Standardsuche des Prologsystems übereinstimmen.
Und zum Schluss machen wir dann die Erweiterung,
dass wir eingebettete Strukturen eben nicht nur für lineare Listen verwenden,
sondern für baumstrukturierte Listen. Das ist ja das Gramm,
was jetzt als nächstes kommt. Verkettete Listen als wichtigste,
rekursive Datenstruktur. Sie sind sehr flexibel einsetzbar als Container,
aber haben eben gegenüber Collections den wesentlichen Vorteil, sie nicht typbeschränkt sind.
Das heißt, die einzelnen Elemente einer Liste können durchaus unterschiedliche Typen haben.
Und der schöne Vorteil ist, es ist eine dynamische Datenstruktur,
die Länge ist nicht begrenzt und ich kann sie jederzeit erweitern bei Bedarf.
Allerdings eingeschränkt, weil sie nur einseitig verkettete Listen sind,
Erweiterung am Listen anfangen, problemlos Erweiterung am Listenende problematisch.
Das ist das Problem, was wir uns dann im Rahmen
der Differenzlisten noch genauer angucken werden.
Da gibt es tatsächlich auch Möglichkeiten, damit umzugehen.
Weil es Ja, ein sehr gutes Beispiel für die Arbeit
mit rekursiven Prädikatsdefinitionen ist. Werden wir uns im weiteren,
also ziemlich viele Beispiele angucken und wir kriegen eben eine spezielle Syntax,
damit wir bequem damit umgehen können.
Wir gucken uns erst lineare Listen an und erweitern das
dann später auf verzweigende Listen, also Baumstrukturen.
Es gibt grundsätzlich zwei Notationsvarianten für Listen, die unterschiedliche Aspekte von Listen hervorheben.
Das erste ist eine iterative Elementaufzählung. Wir können also beispielsweise
oder so taktisch haben wir ja Listen bereits gesehen,
als das Ergebnis von Find All mit einer öffnenden Ecke in Klammer und einer erschließenden Ecke in Klammer.
Und wenn es mehrere Elemente da drin sind, werden sie durch Komma getrennt.
Also iterative Aufzählung heißt, ich schreibe alle Elemente der Liste
durch Komma getrennt in diese Ecke in Klammern hinein.
Der Spezialfall der Lehrenliste ist natürlich auch existent, eine Liste,
die keine Elemente enthält. Und ich kann das Ganze neben
der iterativen Aufzählung auch rekursiv definieren. Rekursiv heißt das immer,
ich brauche ein Anfangselement, das ist unsere leere Liste.
Die leere Liste ist eine Liste und ich brauche eine Nachfolgerbeziehung,
die sagt, ein geordnetes Paar und das ist das Wesentliche,
es ist ein Paar aus einem Element und einer Liste
ist wieder eine Liste
Und der Domänenabschluss nur das, was so gebildet ist,
sind tatsächlich Listen. Wenn Sie sich die Definition angucken,
sollte auffallen, dass die bis auf eine Stelle exakt
das Gleiche ist, was wir für die PA-No-Zahlen hatten.
Nur, dass wir hier jetzt eben den Begriff des geordneten Pass
haben und nicht mehr die einstellige Essstruktur, sondern eine zweistellige Struktur.
Wenn wir diese rekursive Spezifikation hervorheben wollen,
dann gibt es dafür eine spezielle Notation aus dem Restlisten,
mit dem Restlistenseparator oder Listenkonstruktor.
Auch hier haben wir wieder Unifikation,
die ist nicht richtungsabhängig und deshalb kann es sowohl als Konstruktor,
als auch als Accessor verwendet werden.
Das ist dieser senkrechte Strich hier. Der sagt,
hier vorn kann unifiziert der Kopf der Liste und hier hinten unifiziert die Restliste.
Das möchte ich an der Stelle schon mal sehr deutlich machen.
Das ist eine andere Listenauffassung, als sie in Java verwendet wird,
weil das hier der rekursive Charakter der Liste hervorgehoben wird.
In Java sind Listen iterative Angelegenheiten. Das sind also
im Prinzip ist genau diese Sicht auf die Liste
Diese rekursive Sicht auf die Liste, als verkettete Liste
ist nicht die Standard-Variante, die in Java betrachtet wird.
Und was insbesondere zur Folge hat, dass hier ein Typunterschied bestehen kann.
Er muss nicht bestehen, aber er kann bestehen.
Das Ding hier hinten ist immer eine Liste, immer.
Wenn das Gesamtding eine Liste ist,
ist auch das Ding hier hinten eine Liste.
Das Ding hier vorn kann eine Liste sein, muss aber nicht.
Das kann auch ein Atom sein. Wenn ich also eine
solche Liste hier nehme, dann ist der Listenkopf dieses A,
die Restliste ist aber die Liste, die B enthält.
Und da sieht man, es ist ein Typunterschied.
Hier vorn ist ein Atom, infiziert worden.
Hier hinten wird eine Liste unifiziert. Ich habe sie mir
jetzt eine Viertelstunde angeguckt. Könnten Sie bitte mal aufhören oder
wenn, dann gehen Sie doch raus.
Sie müssen sich wirklich nicht hier unterhalten. Also ich bin
ja wirklich ein sehr geduldiger Mensch, aber irgendwann reicht es mal.
Wir können die Notation auch gemischt verwenden,
indem wir nicht nur einen Listenkopf betrachten, sondern mehrere Listenköpfe betrachten.
Wir können also so etwas als Listenpattern schreiben und das heißt dann,
wir interessieren uns für die ersten drei Elemente der Liste und danach
gibt es möglicherweise noch eine Restliste mit weiteren Elementen der Liste
Ich glaube, Frau Dreschler hat es bereits gemacht.
Was ist die Veranschaulichung für Listen? Ich vermute,
sie hat auch die Toilettenpapierrolle mitgebracht. Hab sie nicht?
Nein, ich habe es getan. Ich,
also Frau Dreschler baut ab, muss ich mal sagen.
Ich glaube, früher hat sie nie darauf verzichtet,
das Anschauungsmaterial mitzubringen. Also, was wir hier haben,
ist die rekrusive Sicht auf die Liste,
der senkrechte Strich als das Messer, was das Element abtrennt.
Das Kopfelement ist auch eine sehr gute Veranschaulichung. Und hier
hinten haben wir eben eine Liste wieder in der Hand.
Hier vorne haben wir den Kopf in der Hand
Das erste Element der Liste. Wir können natürlich auch für
höheren Bedarf eins, zwei, drei Köpfe abtrennen.
Das ist jetzt nicht so ganz, die werden hier tatsächlich
dann abgerissen. Ja, wir haben drei Variablen,
auf die wir diese drei Blätter instanzieren können.
Und wir haben eine Variable, das ist die Restliste,
auf die der Rest instanziert werden kann. Was nicht so schön veranschaut,
das ist die funktionale Sicht auf Toilettenpapier und Listen,
was nicht so gut zu veranschaulichen ist, ist das Umgekehrte.
Ich habe da ein Blatt und ich habe eine Restliste und
ich möchte daraus eine Gesamtliste zusammensetzen.
Dann brauche ich Theserfilme oder irgendetwas. Das ist nicht so einfach zu machen,
aber im Prinzip ist das Bild,
glaube ich, sehr, sehr gut,
deshalb bringe ich es auch mit
Über die Restliste kann ich nichts anderes aussagen,
als es ist eine Liste, wieder mit beliebig vielen Elementen,
wie viele Blätter hier noch drauf sind, das weiß ich nicht.
Alles, was ich tun kann,
ist Elemente am Beginn der Liste abzutrennen oder hinzuzufügen.
Das ist das, was die Listenunifikation uns tatsächlich ermöglicht.
Alles andere ist nicht möglich. Das heißt also,
wenn wir jetzt hingehen und Programme entwickeln wollen für diese Listen,
dann müssen wir immer durch diese Brille der Unifikation gucken und
die Brille der Unifikation ist durch dieses Listenpattern gekennzeichnet.
Ich kann immer Elemente am Kopf der Liste abtrennen und bekomme
dann eine Restliste oder ich gebe eine Restliste und etwas,
was ich hinzufügen will, aber immer nur am Anfang der Liste.
Niemals am Ende der Liste, da müsste ich da drin bei der Papphöhle anfangen.
Das geht schlecht. Ohne die gesamte Liste auftröseln zu müssen.
Und das ist tatsächlich der Fall, wenn ich hinten ein Element hinzufügen will,
bleibt mir nichts anderes übrig, als die Liste aufzurollen.
Was heißt rekursiv abzuarbeiten, bis ich am Listen Ende Indikator
angekommen bin, der leeren Liste.
Und dort kann ich manipulieren und dann versuchen die Liste wieder zusammenzusetzen.
Das ist mit dem Anschauungsmaterial nur schwer zu realisieren.
Implementiert ist das Ganze tatsächlich im System durch solche Zellen mit zwei Elementen,
wobei erste Element der Zelle immer auf das Element verweist der Liste.
Und das zweite, die zweite, der zweite Kasten auf die Restliste verweist.
Dort stehen also Pointer tatsächlich drin und auf diese Art und Weise ist also eine einer Liste dadurch gekennzeichnet,
dass sich eine solche nach dem,
nach der Funktion in Lisp oder Scream,
auch Kunstzelle genannten Zelle,
dass ich vorne in der ersten Hälfte einen Pointer auf das Listenelement einfüge und in die zweite Hälfte einen Pointer auf die Restlist einfüge.
Im Falle der einer Liste, die nur ein Element enthält,
ist, dass also die ein der Listen-Ende-Indikator,
der typischerweise als Nil geschrieben wird.
Wenn es nur Nil ist, das Atomnil, entspricht das der Lehrenliste
im Prolog. Ich kann Listen eben beliebig lang machen,
indem ich immer auf Restlisten verweise und habe dann Zweierlisten oder Dreierlisten zur Verfügung.
Und dann sieht man auch die Analogie zu dem Toilettenpapier,
das hier ist sozusagen das erste Element,
das kann ich abtrennen oder auch hinzufügen.
Und dann kommen noch beliebig viele weitere Elemente bis hinten
irgendwann mal der Listenabschluss kommt,
der uns sagt,
die Liste ist vollständig
Irgendwas hat jetzt leider Gottes mein Layout zerhalzt. Ich hoffe,
es kann man noch einigermaßen lesen, dass hier ist also eine Liste,
die als erstes Element ein B enthält und dann eine Restliste.
Die Restliste ist wieder eine Liste,
die als zuerstes Element ein L enthält und dann eine Restliste.
Die Restliste enthält als erstes Element ein A und als zweites Element die Lehreliste.
Und das ist also dann die völlig Synonym zu der Liste,
die B, 1,1 enthält. Das sind alternative Schreibweisen für Listen
, da gibt es überhaupt keinen Unterschied dazwischen.
Und inner, intern ist es eben genau diese Zellenstruktur,
die dort realisiert wird
Während der ganze Unterschied, der zwischen diesen beiden Notationsweisen existiert,
ist eben nur, dass sich hier diese iterative Spezifikation der
Liste verwende und hier die rekursive Spezifikation der Liste verwende,
die immer aus Listen, Kopf und Restliste besteht.
Während ich hier einfach die Listen-Elemente aufzähle. Gut,
wir brauchen einen Begriff für eine Semantik für diese Unifikation von Listen
, weil wir jetzt ja eine spezielle Syntax haben.
Und wie gesagt, die Syntax ist nur eingeführt worden,
damit wir bequem damit arbeiten können. Intern sind es zweistellige Strukturen,
die auch wieder nach den frühen List-Systemen, auch als Dotted Pair bezeichnet werden,
weil sie intern immer die Punktstrukturen gewesen sind.
Bis zur vorletzten Version von SWI-Prolog war es auch auf der Oberfläche noch als Punktstruktur abfragbar.
Man konnte also Listen mit einer zweistelligen Struktur unifizieren,
die den Funktor Punkt hatte, weil es ein Dotted Pair ist.
In der Zwischenzeit haben sie das geändert, die Listen haben jetzt eine Spezialimplementation,
weil die Notationsweise für die aus der objektorientierten Programmierung kommt,
mit dem Punkt, für die Methodenangabe,
die wollten sie auch in Prolog einführen
Und deshalb ist der Punkt jetzt reserviert für was anderes. Das ist also,
man kann es jetzt nicht mehr mit dem Prolog-System nachvollziehen,
aber tatsächlich im Internen ist es immer noch das Gleiche.
Ja. Es gibt das analog zu Ness im Produkt,
auch im Trouble Lists. Ja, klar, ja. Ja,
die können wir ganz einfach bauen, indem die Bedingung,
die wir jetzt für Populists gemacht hatten, war immer,
wir haben ein Element und wir haben eine Restliste.
Das ist vollkommen identisch mit der einer Liste,
die A enthält.
Wie können wir
eine Improper-Liste konstruieren
Die Bedingung war immer hier vorn, muss irgendein beliebiges Element stehen.
Hier hinten muss eine Liste stehen. Wir brauchen nur
diese Bedingung zu verletzen. Wie können wir es machen?
Wir geben einfach an. Oder wer es anspruchsvoller haben will.
Das letzte Element ist nicht mehr die Dialistenabschlussindikator,
sondern ein Atom, ein beliebiges Atom.
Und dann ist es
eine Impropulist
Und da sehen wir auch, das ist im Prinzip wieder
eingepunktetes Paar in Skim. Das, was hier steht. Gut,
der Unifikationsbegriff sagt, zwei Listen unifizieren, ach so,
je nach Repräsentation können wir das jetzt wieder iterativ oder rekursiv sehen.
Iterativ, also Elemente, Aufzählung, heißt es,
wir müssen paarweise die Elemente der Liste unifizieren und die Unifikation schlägt zu,
immer dann fehl,
wenn eine paarweise Unifikation nicht möglich ist
Und das kann aus verschiedenen Gründen passieren.
Es kann passieren, dass die Listen unterschiedlich lang sind.
Ich kann zwar das A mit dem A unifizieren,
das B mit dem B unifizieren,
aber für das C habe ich keinen Unifikationspartner mehr.
Deshalb scheitert die Unifikation hier. Es kann auch passieren,
weil die Unifikation nicht möglich ist.
Also hier auf der ersten Argument der, sorry,
auf der ersten Listenposition binde ich X an A.
Auf der zweiten Listenposition hat das X den Wert A und
das unifiziert nicht mit dem B hier.
Deshalb scheitert die Unifikation. Also entweder die Listen sind
unterschiedlich lang oder irgendwo ist ein Paar von Listen-Elementen,
was nicht mehr unifiziert
Das haben wir eben gesehen. Ich kann bayablen instanzieren.
Hier wird auf der ersten Listenposition Y mit A
instanziert und auf der zweiten Listenposition X mit B.
Und diese Wertebindung kriege ich auch ausgegeben auf der Konsole.
Und c unifiziert mit C. Und deshalb unifizieren diese beiden Listen
miteinander unter dieser vaiablen Bindung.
Und wir vollständig ins spezifizierte Listen gilt natürlich,
ich muss die Elemente miteinander unifizieren.
Und für Atome heißt das eben, sie müssen identisch sein.
Was wir hier auch sehen, ist,
dass wir jetzt mit Listen Datenstrukturen haben,
die Vagablen enthalten können
Ja, hier ist eine Variable drin und hier ist eine Variable drin,
die noch gebunden werden können. Das ist auch etwas,
was, oder ist wieder etwas,
was die Logikprogrammierung von den anderen Programmierpradikmen unterscheidet,
dass Listen oder das Datenstrukturen im Allgemeinen eben Variablen enthalten können,
ungebundene Variablen und dadurch einen prozeduralen Charakter bekommen.
Daten sind nicht mehr etwas rein Passives,
sondern sind etwas, was einen aktiven Charakter hat,
denn sie können über die Unifikation Variablenbindung herstellen und damit
Ergebnisse produzieren
Wir hatten das ja gesehen,
eben bei der Definition von dem größten gemeinsamen Teiler,
wo wir zum Schluss einen Fakt haben,
der nur die Unifikation von drei Argumenten fordert.
Und auf die Art und Weise einen prozeduralen Charakter bekommt.
Die rekrusive Sicht heißt immer, zwei Listen unifizieren,
wenn die Listenköpfe miteinander unifizieren und die Restlisten miteinander unifizieren.
Und die Unifikation schlägt dann eben fehl, wenn der Unifikationspartner überhaupt
keine Liste ist oder aber der Listenkopf beziehungsweise die Restliste nicht miteinander unifizieren.
Also diese Liste mit iterativer Aufzählung der Elemente unifiziert auf dieses
Kopf-Restlisten-Pattern heißt, das X wird an A gebunden,
Y wird an die Liste BCD gebunden.
Und das, da kann ich nicht deutlich genug darauf hinweisen,
ist immer wieder eine Quelle für Verwechslung
Ja, hier hinten, wenn der Input eine richtige Liste war,
muss auch die Restliste wieder eine richtige Liste sein.
Es geht nicht anders. Ich kann das natürlich mit so einem Pattern machen.
Ich interessiere mich nur für die ersten beiden Elemente der Liste.
Die gesamte Restliste ist mir egal. Und dann kriege ich eben
X an dieses erste Element gebunden B an das zweite Element
gebunden. Das sind die Unifikationsvarianten, die wir haben.
Und durch diese Brille müssen wir jetzt immer schauen,
wenn wir auf die Testfälle schauen,
die wir jetzt in einem Programm verwenden wollen
Man kann die Listen-Unifikation,
wie immer die Unifikation jetzt in beiden Richtungen verwenden, je nachdem,
ob die Partner für die Ionifikation instanziiert sind oder nicht instanziiert sind,
baue ich eine Struktur auf oder ich zerlege eine Struktur.
Hier ist also der Fall erst mal des Zerlegens.
Ich habe eine Liste gegeben und ich habe einen Listen-Petter,
mit dem ich unifiziere und dieses Listen-Pettern zerlegt mir die Struktur und gibt mir das erste Element zurück.
Ich kann das natürlich so machen und auf die Art und Weise
ein Pattern haben, was mir das dritte Element der Liste zurückgibt.
Alle anderen sind anonyme Vayablen und deshalb uninteressant. Ich kann genauso gut Listen zusammenbauen,
Komposition mit der gleichen Unifikation wieder, indem ich sage,
füge zu der Liste L, noch das Element Karl hinzu,
indem du, und nenne das ganze Ergebnisliste.
Und die Ergebnisliste ist dann tatsächlich die ursprüngliche Liste erweitert,
um Karl am Listen Anfang. Das ist also relativ einfach,
Erweiterung am Listenanfang ist immer einfach, wenn ich ohne ein rekursives Prädikat,
den Karl an der dritten Position einfügen will,
dann müsste ich eben ein Pattern schreiben,
was mir erst die Liste zerlegt.
In erstes Element, zweites Element und Restliste.
Und hier dazwischen soll ja jetzt das neue Element Karl.
Und dann baue ich die Ergebnisliste entsprechend zusammen,
indem ich sage, das alte erste Element,
das alte zweite Element,
dann mein Karl und dann die Restliste
Und dann erscheint der Karl tatsächlich im Ergebnis hier an der dritten Position.
Also das sind mögliche Varianten, wie ich aus gegebenen Elementen
Listen konstruieren kann. Das Ganze funktioniert natürlich auch,
wenn ich rekursiv eingebettete Listen habe, wo Listenelemente wieder Listen sind.
Das ist ja nicht ausgeschlossen. Das ist zulässig. Dann
habe ich hier jetzt eine Liste, wo der Kopf der
Liste wieder eine Liste ist, gefolgt von einer Restliste.
Und das soll ich unifizieren mit diesem Pattern hier.
Dann muss ich eben der Kopf der Liste hier,
mit dem Kopf der Liste hier unifizieren.
Und die kriegen die vajablen Bindung.
X ist gleich Liste AB
Und dann muss ich die Restliste, die hier übrig bleibt,
nachdem ich dieses erste Element entfernt habe,
mit der Liste übrig bleibe, ionifizieren, die übrig bleibt,
wenn ich hier das erste Element entfernt habe.
Und das ist die Restliste AB.
Also das X hat ja jetzt den Wert, Liste AB,
das unifiziert mit der Restliste hier und deshalb gelingt die Unifikation.
Und das muss man jetzt auch für komplexe,
komplexe Strukturen stur durchhalten und nicht die Nerven verlieren.
Das heißt, wir können jetzt hier hingehen und sagen,
okay, wir haben Hier, iterativ spezifizierte Listen,
wie viel haben wir links, wie viel haben wir rechts?
Elemente, sorry, wie viele Elemente hat die Liste auf der linken Seite?
Wie viele Elemente hat die Liste auf der rechten Seite?
Jeweils drei. Das heißt also, das sieht schon mal
gut aus, paarweise Zuordnung ist möglich.
Und dann können wir eben auf der ersten Listenposition unifizieren,
dass O den Wert B,
also die Liste B,
Q hat
Auf der zweiten Listenposition müssen wir P, Komma Q,
also die Liste P, Komma Q mit der Liste B,
also Liste CP unifizieren.
Damit kriegen wir vajablen Bindungen für P, gleich B.
Für Q kriegen wir die Unifikation Kuh muss C, Komma
P sein. Kuh ist gleich Liste C, Komma P.
Und da wir den Wert für P schon kennen,
wissen wir also, dass das die Liste C,
Komma B ist
Schreien so auf, wenn ich mich vertue.
Auf der dritten Argumentposition trifft die Liste,
die A und Vayable O enthält, auf eine Liste,
die von auch ein A enthält, das auf der ersten Listenposition unifiziert.
Also das O wird auf die Liste,
die mit dem B da beginnt, unifiziert.
Das haben wir ja hier bereits. Ich schreibe das einfach
mal. Links ist ja symmetrisch. Also O soll gleich
der Liste
B-Komma-Liste R-Komma B sein
Damit haben wir hier jetzt wieder eine neue Unifikationsforderung.
Wir müssen das B mit dem hier unifizieren. Das gelingt
. Wir müssen das Kuh, mit dem hier unifizieren.
Das heißt, hier kriegen wir jetzt ein,
soll gleich R, Komma B sein.
Das muss mit dem unifizieren. Das heißt,
wir kriegen als neue Variablen Bindung R gleich C heraus.
Sind wir durch? Ja, wir sind durch.
Dann haben wir alles. Also immer wieder entstehen durch Co-Referenz
neue Unifikationsforderungen, die irgendwie erfüllt werden müssen.
Und wenn alles sich durch Unifikationen substituieren lässt,
haben wir zum Schluss eine Variablenbindung.
Wenn da an irgendeiner Stelle ein Clash auftritt,
dann scheitert die Unifikation
Gut, ja, das können wir gerade noch machen. Unser erstes,
einfachstes Programm, was wir schreiben wollen, ist, dass Programm K ins Scheme,
oder ich habe es hier first genannt, gib mir das erste Element einer Liste.
Wir schauen uns das gewünschte Verhalten an und sagen, ja,
das erste Element der Liste, die nur A enthält,
soll A sein, das erste Element der Liste,
die AB enthält, ist ebenfalls A,
das erste Element der Liste, die A, B,
C enthält es ebenfalls A. Negative Beispiele,
das erste Element einer leeren Liste ist nicht A,
das erste Element der Liste,
die A,
B enthält,
ist nicht
Wir gucken uns wieder die positiven Fälle an,
wie wir das bei dem PEA-Notzahlen gemacht haben.
Wir schauen uns die positiven Fälle an und versuchen, generelles daraus zu generieren.
Und das ist in diesem Falle trivial. Weil immer das,
was hier steht, gleich dem ist,
was als erstes in der Liste steht, Herrgott,
ja, was ist ja auch keine sehr anspruchsvolle Prozedur.
Und das schreiben wir auf. Das schreiben wir genau hier auf,
was immer an der ersten Position der Liste steht,
ist das gesuchte erste Element.
Und hinter, nachdem wir dieses erste Element abgetrennt haben,
kann beliebig viel anderes folgen. Hier, die Lehreliste,
hier die Einerliste, die B enthält, hier die Liste,
die BC enthält, das interessiert uns gar nicht.
Wir wollen nur
das erste Element haben
Und damit haben wir ein Programm geschrieben,
das ist genau die Funktion K, nur jetzt eben richtungsabhängig.
Wir haben K nur benutzt werden kann, um das erste Element
der Liste zu bekommen, können wir jetzt natürlich auch Listen generieren.
Ja, wir können das uninstanziert aufrufen und sagen,
gib mir alle Listen, die A als erstes Element haben.
Und dann würde ich erwarten, dass er anfängt, solche Listen zu generieren,
was in diesem Falle durch Unterspezifikationen sehr, sehr einfach möglich ist.
Ich kriege das eben als Ergebnis, was mir sagt,
nimm eine Liste und unifiziere hier irgendwas hinten rein,
dann ist das erste Element dieser Liste A.
Und das gilt ja auch, wenn das hier hinten gar keine Populist ist,
Es ist trotzdem das erste Element dieser Liste.
Also auch hier wieder beide, oder alle Instanzierungsvarianten funktionieren,
ich kann das erste Element der Liste abfragen,
ich kann mir alle Listen generieren lassen,
die das A als erstes Element enthalten.
Ich kann es auch vollständig unterspezifiziert aufrufen. Das ist das hier unten,
dann kriege ich eben die Informationen, ja,
das X ist irgendein X1 und für das Y muss gelten,
dass das X1 das erste Element dieser Liste ist.
Das ist die, sind die Konstraints,
die für alle bayablen Belegungen von X und Y erfüllen müssen,
gegeben, Prädikatsdefinitionen, die wir haben.
Der interessante Fall ist dieser Fall. Hier, das ist
auch eine partiziell unterspezifierte Anfrage. Hier habe ich jetzt zweimal
eine Variable drin. Das ist nicht, sind nicht unterschiedliche
Varianten wie hier, sondern es ist die gleiche Variable.
Hier fordere ich jetzt Co-Referenz zwischen diesen beiden Argumentpositionen.
Und ich kriege als Ergebnis,
das ändert sich auch mit der Zeit von Implementation zu Implementation.
Ich hoffe, das ist die aktuelle Variante. Ich habe
es nicht mehr überprüft. Ich kriege jetzt als Ergebnis,
ich ja, das ist die Liste X,
die X als erstes Element enthält
Also eine Liste, die sich selbst als Element enthält.
Wir kriegen hier durch Unifikation von endlichen Thermen, kriegen wir eine unendlich zyklische Struktur. Das ist das,
ja, wir können auch hier wieder,
wir hatten uns das bei den PA-Notzahlen schon angeguckt,
auch da konnten wir zyklische Strukturen erzeugen,
dass es jetzt hier wieder genau der gleiche Effekt,
wenn wir das Ganze uns ausgeben lassen,
dann kriegen wir eben eine solche Hilfsnotation, die uns irgendwie sagt,
ja, das ist ein typischer Ausdruck,
der dort erzeugt wird.
Wir können uns auch Wieder, wie bei den PA-No-Zahlen,
versuchen, die Elemente auf den unterschiedlichen Rekursionsebenen auszugeben.
Also gib mir das erste Element dieser Liste,
gib mir in der eingebetteten Liste wieder das erste Element.
Gib mir in der Zweifach eingebetteten Liste auch wieder das Element.
Und wir sehen, egal was wir machen, es ist alles gleich.
Es ist immer wieder das gleiche Ding, was wir dort vorfinden.
Wir haben auch hier wieder eine zyklische Struktur erzeugt. Gut,
also an der Stelle sehen wir schon,
dass es eine hochgradio Analogie zwischen den PA-No-Zahlen und den Listen gibt,
nur dass eben PA-No-Zahlen einstellige Strukturen gewesen sind.
Hier haben wir zweistellige Strukturen vorliegen. Wir haben hier einen
Zyklus eingebaut, das erste Element in sich selbst einbettet.
Wir können natürlich genauso gut zyklische Listen erzeugen,
die also jetzt über diese Zellenstruktur uns irgendwann mal wieder auf hier vorne vorbei sind.
Wir können sich zu Hause mal überlegen,
was man machen müsste, um so eine Struktur zu erzeugen.
Aber es geht natürlich. Wir können durch einfache Unifikationen erreichen,
dass eine solche zyklische Liste aufgebaut wird. Das,
was wir im Augenblick hatten, in diesem Falle,
ist immer nur, dass das erste Element einer Liste,
die Liste selbst wieder ist.
Diesen Zyklus haben wir aufgebaut. Jetzt geht es darum,
hier hinten einzutragen, den Verweis auf diese Zelle wieder
Ja, also das kann man so machen,
das kann man das andere machen.
Für diesen Fall kenne ich keine sinnvolle Verwendungsweise für zyklische Listen,
kenne ich sehr wohl, sinnvolle Verwendungsweisen.
Gut, dann sind wir am Ende angekommen,
dann danke ich für die Aufmerksamkeit,
wünsche ich ein schönes Wochenende und bis nächsten Freitag