Teilnehmerzahl zufolge ist es ein harter Jahreswechsel gewesen,
den ich viele überlebt habe oder aber es ist das heutige Datum,
was viele davon abgehalten hat, das Bett zu verlassen.
Da kann man jetzt treffig drüber spekulieren, ehe ich
es vergesse, ist Herr Pietz zufällig unter den Anwesenden.
Nein, okay, gut, dann muss ich das
per Mail erledigen
Okay, wir hatten in einem der vorigen Jahre uns angefangen mit Metaprogrammierung zu befassen,
Metaprogrammierung als die Kunst, Interpreter zu schreiben,
die die Programmiersprache oder die Ausdrücke der eigenen Programmiersprache entgegennehmen und nach irgendwelchen festgelegten Regeln verarbeiten,
was natürlich nur Sinn macht, wenn die Regeln,
nach denen verarbeitet werden sollen,
von den Standardregeln in irgendeiner Form abweichen.
Und wir hatten uns einfache Formen solcher Metainterpreter
schon mal angeguckt.
Oh Gott
Ja. Dies ist der einfachste Meta-Interpreter dieser Art. Wir vereinfachen Prolog natürlich
ein bisschen von der Sonntags, damit wir das hinbekommen.
Wir haben immer die Grundstruktur. Entweder haben wir die konstante
True, die immer zu einem Erfolg der Ableitung führt.
Oder aber es ist eine Konjunktion von Teilzielen vorgegeben.
Dann müssen wir zuerst das linke Teilziel abarbeiten und dann das
rechte Teilziel abarbeiten. Da Teilziele rekursiv ineinander eingebettet werden können,
geht das also dann auf beliebige Sequenzen von Teilzielen zu erweitern.
Und der dritte Fall schließlich ist, wir haben einen Teil viel vorgegeben.
Wir suchen eine passende Klausel in der Datenbank.
Wir instanzieren den Körper der Klausel und wir rufen den Körper
der Klausel als neues Teilziel auf.
Das ist die gesamte Essenz der Verarbeitung von Prolog und wir können
das jetzt in verschiedener Weise ausnutzen, indem wir das modifizieren.
Achso, ja, das war jetzt nur noch der Überblick.
Wir haben den Meta-Interpreter, der auf der Meta-Ebene auf einem Prolog-Programm draufschaut.
Wir geben ihm also ein Prolog-Programm zur Verarbeitung und er nimmt
jetzt immer aus den gegebenen Zielen,
die wir dem Programm übergeben,
sucht er sich passende Klauseln auf dem Basislevel aus
und arbeitet die dann ab,
indem er
den Körper instanziert
Gut, die erste Variante, die wir hatten, ist nur eine Ablaufprotokollierung gewesen.
Da ändern wir ja die Somantik des Programms, als solches noch nicht,
denn die Berechnungsergebnisse sollen ja die gleichen sein,
als wenn wir nicht protokollieren.
Alles, was wir einfügen, sind Nebeneffekte,
das auf der Konsole Details der Verarbeitung des Programms ausgegeben werden.
Das Ganze wird dadurch erreicht, dass wir,
um die rekursive Einbettung zu veranschaulichen, eine Einrücktiefe haben,
die wir am Anfang initialisieren müssen und die bei jedem bei jedem rekursiven Aufruf,
um einen bestimmten Betrag erweitert wird.
Sodass also immer, wann immer es eine Rekursionsebene weiter unten geht in der Verarbeitung,
mehr Leerzeichen auf der Konsole erscheinen und der Aufruf weiter eingerückt wird.
Ansonsten hat sich nicht viel geändert. Wir müssen jetzt nur
immer diese Eindrücktiefe weiterreichen. Wenn wir bei der Konstante True
angekommen sind, dann brauchen wir die Eindrücktiefe nicht mehr.
Da gibt es nichts zu protokollieren. Ansonsten wird sie eben
immer nur durchgeschleift über die Ebenen. Mit Ausnahme hier,
wo sie durch den Reco-7-Aufruf erweitert wird.
Ansonsten bleibt alles genauso. Wir suchen eine relevante Klausel in
der Datenbank
Geben die Einrücktiefe auf dem, auf dem,
auf der Konsole aus. Wir schreiben das Ziel,
was gerade verarbeitet wird, auf die Konsole und ansonsten,
wir erweitern die Einrücktiefe und rufen das Ganze wieder mit der
neuen Einrücktiefe und dem instanziierten Körper der Klausel auf.
Das, was wir dann erreichen, sieht so aus,
dass wir jetzt immer unseren Metainterpreter aufrufen mit einem Ziel für eine Prädikatsdefinition,
die in der Datenbank steht und wir kriegen
jeden Aufruf protokolliert bei jedem,
in jedem Falle durch die Einrücktiefe
weiter nach innen gerückt
Wenn wir eine erfolgreiche Ableitung gefunden haben, dann meldet sich jetzt wieder
unser Produktsystem zu Wort und gibt uns die vajable Bindung aus.
Also das sind Aktionen des Metainterpreters, das ist eine Aktion unseres Produktsystems.
Da sieht man also diese Verschachtelung der Verarbeitung. Und wir können jetzt natürlich ganz normal,
wie immer nach Alternativen fragen und dann wird uns unser Metainterpreter protokollieren,
dass er versucht hat, hier fehlt noch ein Komma,
dass er versucht hat, diesen Aufruf nochmal zu machen,
aber zu keinem weiteren Ergebnis gekommen ist.
Das ist noch relativ sparsame Protokollierung, weil wir nicht so richtig sehen,
mit welcher Klausel hat er da diesen Aufruf gemacht?
Mit welchen Klauseln ist er gescheitert? Wenn wir also eine
etwas aussagekräftigere Protokollierung haben wollen, müssen wir radikaler herangehen.
Also, was wir zum Beispiel sehen wollen,
welche Klauseln sind zum Vergleich herangezogen und was sind die Unifikationsresultate?
Also ist das die Unifikation erfolgreich gewesen oder nicht?
Und wir wollen insbesondere auch die erfolglosen Klauselaufrufe sehen.
Das ist das, was der eingebaute Prolog Tracer ja nicht tut.
Das ist also eine Erweiterung der Funktionalität,
die wir uns selber bauen können
Und wenn wir das haben wollen, dann müssen wir
zwei Sachen trennen. Wir müssen weitere Stücke der Verarbeitung,
dem Prologsystem aus der Hand nehmen und in eigene Regie durchführen.
Und das ist insbesondere die Suche nach einer relevanten Klausel,
denn wir wollen jetzt nicht nur dann,
wenn die Unifikation des Klauselkopfes erfolgreich war, ein Protokolleintrag erzeugen,
sondern wir wollen wissen, welche Klauseln gibt es überhaupt,
welche davon unifizieren erfolgreich und welche unifizieren nicht erfolgreich.
Das heißt, wir dürfen nicht von vornherein der Prolog-Verarbeitung überlassen,
so wie wir das hier noch getan haben
Wir haben ja hier nach wie vor dieses eingebaute Prädikat verwendet,
was im Prinzip die Poloc-Suche ist,
die uns gegeben ein Ziel, einen Körper liefert,
aber den Körper sofort instanziert.
Und das ist das, was wir dem System aus der Hand nehmen müssen.
Das dürfen wir dem Prolog nicht mehr überlassen.
Das müssen wir jetzt in Eigenregie durchführen. Gut,
hier ist eine Lösung, wie wir das machen können.
Es heißt natürlich, wir müssen erstmal Klauseln suchen,
ohne dass wir bereits instanzieren
Damit wir sehen können, gelingt die Unifikation,
gelingt sie nicht. Und auf die Art und Weise,
wir müssen also in jedem Falle erstmal ein Teilziel erzeugen.
Nehmen wir mal an, wir haben ein Teil, sie zu verarbeiten.
Das sieht so aus. Dann müssen wir das umwandeln,
wenn wir das in die normale Verarbeitung reinstecken würden,
wird x sofort instanziert.
Das müssen wir vermeiden. Wir müssen das also umwandeln in
ein Teilziel, was neue Variablen hat, die uninstanziert sind.
Mit diesem Teilziel können wir in die Prolog-Suche gehen und
wir kriegen alle Klauseln geliefert,
die ein zweistelliges P-Prädikat definieren
Und können jetzt anschließend fragen, ja,
wenn wir die Klausel wissen, dann unifizieren wir doch mal
bitteschön mit diesem Aufruf hier und dann wissen wir,
ob diese Infikation erfolgreich ist oder nicht.
Aber wir müssen es erstmal uninstanziiert aufrufen, um überhaupt alle
Klauseln geliefert zu bekommen. Das ist der Trick dabei.
Und um genau dies zu erreichen, ein uninstanziiertes,
uninstanziiertes Teilziel zu erzeugen, gibt es im Prinzip zwei Möglichkeiten.
Die eine ist hier skizziert,
die andere ist hier skizziert, die Variante hier sagt,
ich nehme mein Ziel und wandle es in eine Liste um
Diese Umwandlung passiert, das ist die Semantik dieses Operators,
dieses Infix-Operators, dass das erste Listen-Element der Funktor ist und
alle weiteren Listen-Elemente, die Argumente dieses Ziels.
Dann ermittle ich mit diesem eingebauten Prädikat die Länge dieser Restliste.
Das heißt, ich weiß dann, wie viele Argumente dieses
Teilziel hat. Dann rufe ich das Ganze umgekehrt auf,
mit uninstanziierten Argumenten, aber mit der gleichen Länge.
Das heißt, jetzt wird hier eine Liste erzeugt,
die genauso lang ist wie diese Liste, aber nur uninstanzierte Variablen enthält.
Also das können wir uns ja mal angucken.
Wenn wir Lenk, uninstanziert, aufrufen. Wir wollen also
eine Liste der Länge 3 haben, dann ist die,
enthält die nur uninstanzierte Variablen.
Und das machen wir uns hier zunutze. Na nun,
komm schon. Wir haben jetzt hier eine Länge,
eine Liste der Länge die aus der Argumentanzahl in dem ursprünglichen Aufruf besteht.
Und jetzt ändern wir diese Liste wieder um,
indem wir nochmal den Funkel davor schreiben und dann wieder in eine Therm umwandeln.
Und dann haben wir genau das, was wir haben wollten.
Der Funktor ist der Alte geblieben,
die Liste der Argumente ist genauso lang, wie sie vorher war,
nur dass eben jetzt alle Argumente uninstanziert sind.
Und mit diesem künstlich erzeugten Ziel können wir jetzt tatsächlich
in die Suche nach einer Klausel gehen.
Das ist hier versteckt in einem Hilfsprädikat, kommt gleich auf
der nächsten Folie. Alternativ dazu können wir natürlich auch dieses
eingebaute Prädikat Funktor verwenden
Wir nehmen das alte Ziel und wir lassen uns den Funkler geben.
Das heißt, das ist der Prädikatsname.
Und wir lassen uns die Stelligkeit geben. Und dann rufen
wir das umgekehrt instanziert auf mit einem uninstanzierten Teilziel,
dem gleichen Funkter wie hier oben und der gleichen Stelligkeit.
Und dann passiert genau wieder das,
dass wir ein Therm erzeugen, der den gleichen Funker hat,
der die gleiche Stelligkeit aber vollständig uninstanzierte Argumente hat.
Also das ist eine Alternative, wie man den gleichen Effekt erzeugen kann.
Wir suchen jetzt eine Klausel mit diesem künstlichen Aufruf.
Wir protokollieren jetzt, welches ursprüngliche Teilziel wir haben.
Wir protokollieren weiterhin, welche Klausel gerade bearbeitet wird.
Das ist nämlich das G2, das ist der Klauselkopf.
Das ist ja das künstlich erzeugte, die künstlich erzeugte Signatur unseres Prädikats.
Und die geben wir aus, weil das ja im Prinzip
der Klauselkopf ist, der klauselt, die wir gerade bearbeiten.
Und dann können wir aus der Form des Klauselkopfs entsehen,
welche Klausel hat das System gerade angefasst. Jetzt kommt,
bis dahin haben wir nur protokolliert, was wollen wir gerade machen?
Das ist der Aufruf E1. Welche Mittel stellt uns das Programm
das zur Verfügung, das ist der Klauselkopf.
Und jetzt können wir hingehen, nachdem wir das protokolliert haben
und können das ursprüngliche Ziel mit diesem künstlichen Ziel unifizieren.
Und dabei wird der Kopf der Klausel, äh,
der Körper, der Klausel instanziert.
Das passiert jetzt hier. Aber erst nachdem wir das protokolliert
haben. Und dann können wir eben sagen,
wenn diese Unifikation erfolgreich ist,
dann geben wir einen Success auf der Konsole aus,
anderen Preis geben wir ein Fail aus.
Und wenn es scheitert, dann müssen wir das Ganze natürlich auch scheitern lassen.
Ansonsten bleibt alles wieder beim Gleichen. Wir müssen die Einrücktiefe
wieder um einen bestimmten Betrag erhöhen und rufen das Ganze
rekursiv auf
Jetzt noch dieses Hilfsprädikat Fine Claws, das könnte man
etwas einfacher machen. Ich habe es hier noch ausgeweitet.
Ich suche hier mit diesem künstlich erzeugten Ziel eine passende Klausel
in der Datenbank und liefere den Körper entsprechend zurück.
Der ist ja nicht instanziert, weil ich ja nur mit uninstanzierten Vayablen unifiziert habe.
Das heißt also, der Körper ist nach wie vor uninstanziert,
aber per Co-Referenz mit dem Kopf verbunden.
Und der Kopf, der wird ja dann später hier unifiziert,
sodass die Instanziierungen von G2 als Instanzierung auf G1 übertragen werden.
Das passiert ja an dieser Stelle
Gut, alternativ dazu und das ist der einzige Grund für dieses eingebettete Prädikat.
Möchte ich jetzt noch protokollieren, wann die Klauseln in der Datenbank
vollständig abgearbeitet sind? Nachdem also das letzte Teilziel gefunden wurde,
wenn das hier kein Ergebnis mehr bringt,
dann kommt er zu der zweiten Klausel hier und die zweite Klausel macht nichts anderes,
als dass sie nochmal mit der alten Einrücktiefe sagt,
die Definition zu diesem Prädikatsnamen ist jetzt abgearbeitet,
mehr Klauseln habe ich nicht zur Verfügung.
Und dann sieht eben das Protokoll so ungefähr aus, wie das,
was ich hier in der Vorlesung immer verwendet habe, leicht modifiziert.
Ich kriege also,
ich rufe diesen Meta-Interpreter wieder auf mit einem Teilziel und ich kriege protokolliert,
ja, ich fasse, ich möchte dieses Teilziel abarbeiten,
also welche Elemente sind in der Liste, die ABC enthält, enthalten.
Ich nehme als erstes die Klausel,
die hier eine Einerliste fordert und aus diesem Klausel,
aus der Signatur dieses Klauselkopfes, kann ich entnehmen,
wo ist das Programm gerade?
Und dann kriege ich ausgegeben ja leider diese Unifikation scheitert,
weil hier eben eine Einerliste gefordert wird und hier eine Dreierliste zur Verfügung steht.
Dann gehe ich eben auf der gleichen Rekursionsebene in die zweite Klausel
hinein und sehe an dem, das an diesem Klauselkopf,
denn jetzt wird ein Kopf und eine Restliste abgearbeitet.
Das ist die zweite Klausel meiner Lastdefinition.
Und diese Unifikation ist erfolgreich, was dazu führt,
dass es zu einem rekursiven Aufruf von My Last kommt.
Das ist also jetzt ein relativ ausführliche Protokollierung,
die deutlich mehr Informationen liefert, als etwa das eingebaute Tracing des Prologs.
Wir sehen auch hier diesen Schlenker, den das Programm noch macht,
wenn ich also das Ergebnis enthalten habe,
Was natürlich ein ziemlicher Unfug ist, was hier steht.
Der Aufruf passt überhaupt nicht zu den... Ja, sorry.
Bitte? Ach so, ja, ist ja Lars,
sorry. Ja, es ist Lars. Danke.
Ich war gerade wieder im falschen Film. Gut,
also nachdem ich das letzte Element gefunden habe,
kann ich natürlich hier wieder die Suche nochmal anstoßen,
weshalb er jetzt zur zweiten Klausel kommt.
Die zweite Klausel unifiziert tatsächlich. Es wird wieder ein Element
der Liste abgebaut, was zu dem Reco-7-Aufruf mit der Lehrenliste führt.
Und dann sehe ich jetzt, dass dieser Aufruf mit
der Lehrenliste in beiden Fällen hier scheitert
Und dann kriege ich eben nur noch ausgegeben, damit sind alle,
alle Klauseln auf der Ebene, auf dieser Rekursionsebene abgearbeitet,
auf der nächst darüberliegenden Rekursionsebene auch und so weiter und so fort,
zu dem Gesamtergebnis, ein weiteres letztes Element gibt es natürlich nicht.
Das sind also Beispiele gewesen,
wie ich die Kernsemantik des Programms nicht angehe,
ich möchte die gleichen Berechnungsergebnisse haben, wie ich sie vorher hatte,
aber ich möchte Zusatzinformationen über die Abarbeitung bekommen.
Jetzt gehen wir einen Schritt weiter,
jetzt ändern wir die Semantik tatsächlich und eine Möglichkeit,
also es gibt zahlreiche Möglichkeiten, wie man die Semantik verändern kann.
Das Beispiel, was ich hier rausgegriffen habe,
ist die Zyklenüberwachung
Mir haben ja immer das Problem, dass wir durch rekursive Aufrufe,
die nichts konsumieren, in ein Zyklus kommen können.
Und das ist sehr unangenehm. Und wir kennen auch
die Lösung für das Problem, das heißt Memuisierung.
Wir müssen irgendwo eine Geschichte mitführen, welche Aufrufe wir in
der Vergangenheit getätigt haben. Und dann können wir überprüfen,
ob ein aktuelles Teilziel irgendwo in der Geschichte schon mal aufgetreten ist.
Und wenn das passiert ist, dann ist offensichtlich die Verarbeitung
in einem Zyklus drin. Und dann können wir diesen Zweig
der Ableitung abbrechen.
Das ist die Idee dabei
Also was hier passiert ist, statt dieser Einrücktiefe wird jetzt
auf dieser Argumentposition eine Geschichte mitgeführt. Eine Geschichte der früheren,
oder in einer, ja, in der Vergangenheit gemachten Prädikatsaufrufe.
Bei den ersten beiden Klauseln ändert sich wieder gar nichts.
Wir geben hier, wir ist die Geschichte wieder uninteressant.
Wir sind ja bei der konstanten Truhe angekommen.
In der zweiten Klausel wird sie einfach nur durchgereicht,
weil ja eigentlich nichts berechnet wird, sondern nur die Verzweigung der Abarbeitung passieren kann.
Und was hier das Entscheidende ist in der dritten Klausel,
wo ich eben jetzt immer,
die das aktuell abgearbeitete Teilziel auf die Geschichte draufschreiben muss,
wenn ich den rekrusiven Aufruf mache.
Hier wird die Geschichte aufgebaut. Und geprüft wird sie hier
. Ich prüfe jetzt also, ob das aktuelle Zeilziel mit
irgendeinem Element der Geschichte vergleichbar ist.
Und ich habe das hier so gemacht,
dass ich noch die Anzahl angebe,
wie oft das dort darauf aufgetreten sein muss
Ich möchte noch nicht gleich abbrechen, wenn es bei einem Mal da auftritt,
sondern wenn es wiederholt auftritt, reicht das ja auch,
dann kann ich immer noch abbrechen.
Das ist die Idee dabei. Das ist ein Hack,
ich gebe es zu. Gut, wir brauchen also
ein paar Hilfsprädikate wieder, wir müssen dieses Not-Unifible testen.
Also wenn das bei null, ich zähle jetzt das N herunter,
bis ich bei null angekommen bin, dann weiß ich,
ich habe N mal dieses Teilziel gefunden und dann kann ich das Ganze abbrechen.
Ansonsten mache ich eben einfach einen rekursiven Abstieg,
dass ich immer teste,
ob das aktuelle Teilziel mit dem ersten Element der Liste unifizierbar ist,
wenn ja,
zähle ich das N1 runter und gehe in die Rekursion
Also das macht eben nichts anderes als zu testen.
Wie oft steht das auf dieser Historie eigentlich drauf? Ein Problem,
was wir ja hatten, waren diese transitiven Regeln, also ein Terminierungsproblem,
was uns ja eine Zeit lang beschäftigt hat, waren diese transitiven Regeln.
Wenn ich also die Transitivität nicht mithilfe eines Hilfsprädikats realisiere,
was dafür sorgt, dass ich erstmal das erste Teilziel,
soweit es geht, an Fakten instanziere, sondern wenn ich sofort,
wie es in der Mathematik ja üblich ist,
das Prädikat mit sich selbst aufrufe
Wenn wir diese Definition verwenden und dann aufrufen,
wovon ist A, das ist ja eine ISA-Herarchie,
eine Instanz, dann kriegen wir sowohl B als auch C.
Und wenn wir wieder nach Alternativen fragen, dann haben wir das Daminierungsproblem im Haus.
Wenn wir jetzt unseren LDI steht für Loop,
die detapting Interpretor aufrufen, dann sehen wir,
dass er nach dem C abbricht,
weil er dann nämlich hier die Schleife läuft und das stellt das Programm fest und bricht die Verarbeitung ab.
Die Frage ist, haben wir damit das Halteproblem der
Touring-Maschine gelöst
Einer hat eine Meinung dazu. Ja, ja, wie
wäre sie denn? Das ganze Putzblatt einfacher zu klären.
Ja, es gibt ja auch noch die Schnittleiter zu.
Die wären? Also zum Beispiel Porse Zukunft ist nicht für die komischste solcher Zeichnung.
Es könnte immer neue Zeichnungen auftreten, sodass auch nicht,
Ja, was heißt, neue Sachen treten auf, ja.
Das erste habe ich nicht verstanden, was war das mit dem...
... Sequenz, also dieses Jahr, mit dem Judisch.
Ja, das hat ja mit der Periodität noch nicht unmittelbar was zu tun.
Eine andere Meinung. Das alte Problem liegt ja auch daran,
dass die Dinge, die Suchbräume öffentlich sind.
Das heiße Problem, wie man sagt,
da werden wir freie Mütter von arbeiten.
Denn im Film heißt das noch lange nicht,
dass wir feststellen können, dass wir keine Lösung haben.
Ja, okay.
Und endlich große
Gut, aber wir brauchen ja das Ganze nur
zu negieren und dann haben wir genau das Problem.
Ja, wenn wir jetzt diesen Aufruf negieren,
dann können wir eben nicht feststellen, ob die Negation gilt,
weil er unendlich lange daran rechnet.
Das ist schon das alte Problem. Aber die Frage ist,
haben wir es gelöst? Also wo ist die wundestelle der Implementation?
Natürlich haben wir es nicht gelöst, das können wir von vornherein,
ja so viel vertrauen sollte man in die Theorie mitbrechen.
Das war jetzt nicht Anfang hier. An der Stelle,
ja. Wir haben in der Zeit den Truppling für ein
einfaches Programm, dann einfach den Zukunftsklus.
Genau, aber was wären die allgemeineren Fälle,
die das Programm nicht abfangen würde?
Ja. Das kann ja auch unendliche Steifen geben,
bei denen wir immer neue, also in diesem Fall Gott,
bei denen wir jetzt sozusagen immer neue Aufrufe haben,
an denen wir zum Beispiel
etwas nicht,
der Zahl hochziehen
Ja, ja, natürlich. Es ist nur,
wir müssen es Unendliches schwierig, ja,
wir könnten das dynamisch erzeugen, das ist richtig,
aber wo es praktisch so fortscheitert, ist indirekte Rekursion.
Jedenfalls so ungeschickt, wie es hier implementiert ist.
Die Implementation ist extrem ungeschickt, weil ich nämlich dreimal hintereinander teste,
ob es mit dem gleichen Aufruf und Iunifizierbar ist.
Das heißt, der indirekte Fall, der indirekten Rekursion ist auch nicht abgedeckt.
Das können wir uns gleich noch angucken. Also,
was wir gemacht haben, ist, dass wir die Inferenzregeln modifiziert haben,
statt Dass wir das Ziel in der Datenbasis abgespeicherten Klauseln ableiten mit der Inferenzstrategie,
schränken wir es ein, falls es nicht einmal in direkter Folge jeweils miteinander unifizierbar ist.
Und dieses indirekter Folge ist, das ist die primitive Lösung hier.
Das könnte man sofort anders machen, indem man also testet,
es muss nur zum Beispiel einmal drauf aufgetreten sein.
Also deckt das Ganze immer nur Zyklen ab,
die durch direkte Rekursion entstehen und es ist auf der anderen Seite zu rigide,
weil ich nämlich Zyklen, die zur Erzeugung von Nebeneffekten dienen,
Ja, also ich will immer nur was auf die Konsole ausgeben,
auch die werden abgefangen.
Auch das ist immer wieder unifizierbar und die bricht er dann auch irgendwann mal ab.
Wir können uns das mit der indirekten Rekulsion angucken,
wenn wir also das ISA nicht direkt auf ISA zurückführen,
sondern es ist eins dazwischenschalten.
Und da haben sie völlig recht, das könnten wir dynamisch erzeugen.
Das Programm können wir ja generieren, indem wir immer in
jedem Rekulsionsschritt neue Klauseln in die Datenbank hineinschreiben.
Hier habe ich es also hart verdrahtet, dass sich das
ISA auf S1 zurückführe und ist eins auf SA zurückführe.
Und dann sieht man in dem Augenblick,
versagt dieser Zyklendetektor
Aber wir könnten ihn ja intelligenter programmieren.
Wir könnten ihn ja so bauen, dass ja jedes,
jede Wiederholung eines Aufrufes über beliebige, lange Strecken hinweg bemerkt.
Hätten wir dann das Halteproblem? Nehmen wir mal für
statische Programme, hätten wir das dann gelöst.
Dann haben wir erstmal nur das Ungeschickte dieser Implementation beseitigt
Also, ich nehme mal aus der Seite des Problems natürlich nicht gelöst.
Okay, das Vertrauen ist ungebrochen, ja. Da merkt man ganz sicher,
dass ich das falsch gedacht habe, was ich gerade dachte,
das kann man jetzt in diesem Algorithmus haben,
der meinetwegen nicht 300 Jahre in keiner Szene draus binden will und den so ungeschickt programmiert,
dass eben immer nach größerem Alternativenzug führt,
dass wir sozusagen dann auch alle Zahlen guten Worten
größer eingezählt sind.
Dann hätten wir sozusagen immer
neue Testfälle
Das heißt, es begäbe gar keinen Zyklus.
Ja. Ja. Gut, also es gibt zahlreiche Möglichkeiten,
wie man das Ganze aus dem Gleichgewicht bringen kann.
Eine andere Möglichkeit ist natürlich, ich kann diesen Zyklus hier beliebig groß machen.
Ja, der muss ja nicht über eine Zwischenstation gehen,
der kann über beliebig viele Zwischenstationen gehen und entsprechend lang wächst dann meine Historie.
Dann wird also der Metaaufwand zum Überprüfen der Historie
immer größer und dadurch konvergiert das Ganze auch,
also kriege ich im Endeffekt auch keinen Halteffekt mehr zustande
Also es ist ein, wie immer man es macht,
man kann sich Geschickte anstellen oder weniger geschickt anstellen,
aber das Halteproblem bleibt natürlich.
Das wird man nicht los auf die Art und Weise.
Aber es kann sehr viele Fälle von Zyklen tatsächlich erkennen und abfangen.
Gut, damit haben wir den Abschnitt Metaprogrammierung abgeschlossen.
Und jetzt kommen wir tatsächlich zu einem Schreiben eines richtigen Interpreters.
Das ist insofern kein Meta-Interpreter, weil er jetzt,
weil nicht ein Prologprogramm, ein Prologprogramm interpretiert,
wie wir das bisher immer gehabt hatten,
sondern weil wir jetzt Ausdrücke einer anderen Programmiersprache
uns vornehmen
Dass es in diesem Fall es geben und damit
die Brücke zur funktionalen Programmierung, die geschlagen wird.
Was wir also hier machen, ist,
wir führen Scheme dadurch ein, dass wir einen Interpreter fürs Scheme schreiben.
Indem wir sukzessive Ausdrücke und ihre Semantik einführen und wir müssen
die Semantik dieser Ausdrücke ja natürlich in unserem Interpreter realisieren.
Und da wir Prolog kennen, wissen wir also,
wenn wir wissen, wie Prolog diese Ausdrücke verarbeitet,
haben wir sozusagen einen indirekten Zugang zur Semantik der einfachen funktionalen Programmiersprache
Ja, wir müssen uns erst mal mit dem Grundbegriffen der
funktionalen Programmierung befassen. Wir müssen dann die ausgebuffteren Sachen einführen.
Das ist insbesondere der Begriff der Umgebung. Das ist ja
etwas, was wir in der Logikprogrammierung überhaupt nicht kennen.
Wir brauchen dann die funktionale Auswertung. Wir müssen die funktionale
Ausdrücke in Umgebungen auswerten können. Und wir gucken uns dann
diverse Beispiele für rekursive Funktionen in der funktionalen Programmierung und auf
Funktionen höherer Ordnung an und versuchen abschließend nochmal einen Vergleich zu
machen zwischen diesen Programmierparadigmen
Als Wiedererkennungsmerkmal habe ich hier etwas gewählt, was ein,
der Kern der funktionalen Programmierung ist, nämlich sogenannte Lamde-Ausdrücke,
Lamde-Ausdrücke sind nämlich nichts anderes als Funktionen,
die nur noch keinen Namen zugewiesen bekommen haben.
Dieser Lamde-Ausdruck ist also eine Funktion eines Arguments,
das steht hier,
es ist ein Lamde von einem Argument und der Körper der Funktion sagt,
was mit diesem Argument zu passieren hat,
um den Funktionswert zu ermitteln
Und solche Landerausdrücke kann ich geschachtelt ineinander einbetten, sodass also
dieser Ausdruck hier sagt, ich bin auch eine Funktion eines Arguments.
Mein Funktionswert berechnet sich aus dem Argument durch Quadrieren.
Und ich wende jetzt diesen Wert hier auf das,
ich bin jetzt nicht ganz sicher, ist das, ja,
ich wende die sind, irgendwie fehlt hier eine Klammer, oder?
Zwei Klammern fehlen die ganzen. Genau, ja. Deshalb
stocke ich gerade. Ich wende also diese Funktion jetzt auf
diese Argumentbelegung an. Ich instanzieriere in den Borten von Prolog
, jetzt das X mit drei
Quadriere es, ermittle einen Funktionswert. Das ist nämlich 3 hoch 2
, also 9. Und ich wende anschließend diese Funktion hier oben
auf den hier ermittelten Funktionswert an. Das heißt also,
ich muss jetzt dieses X hier mit neuen instanzieren,
addiere 2 drauf, sodass insgesamt der Funktionswert dieses Ausdruckes 11 ergibt.
Das ist der Kern der funktionalen Programmierung. Ich habe Funktionen,
ich wende sie, das ist die sogenannte Applikation,
ich wende sie auf Argumente an,
erhalte dadurch einen Funktionswert und dieser Funktionswert kann wieder in andere
Funktionen eingehen,
wenn die Aufrufe
rekursiv ineinander eingebettet sind
Gut, Modulo, der Klammern, die muss ich nochmal ergänzen.
Gibt das ungefähr einen Eindruck, was funktionale Programmierung ist?
Also im Gegensatz zur Relation,
die wir ja als Teilmenge des Kreuzprodukts einer Domäne mit sich selbst
definiert hatten oder für das Kreuzprodukt zwischen verschiedenen Domänen eingeführt hatten,
ist also eine Funktion, immer eine eindeutige Abbildung.
Einer der zentralen Unterschiede zur Logikprogrammierung,
bei der Logikprogrammierung haben wir es eben immer wegen des relationalen Charakters,
im allgemeinen Fall mit mehreren Berechnungsergebnissen zu tun.
Bei der funktionalen Programmierung ist es die Eindeutigkeit des Ergebnisses garantiert.
Das ist im Berechnungsmechanismus verankert
Die üblichen Schreibweisen sind also immer,
Y ist eine Funktion eines Arguments hier oder wir sagen,
dass diese Menge hier in diesem Funktionsbereich abgebildet wird.
Applikation hatte ich schon erwähnt,
ist also der Prozess der Auswertung funktionaler Ausdrücke,
in dem ich Funktionsdefinitionen auf Wertetupel anwende.
Wobei die Wertetupel eben immer die Größe haben müssen,
wie die Argumentanzahl meiner Funktion ist.
Und dann heißt Applikation eben immer,
nimm die Argumentbelegungen und ermittle einen Funktionswert.
Dafür gegeben die Funktionsdefinition
Und damit ist funktionale Programmierung anders als die Logikprogrammierung,
wo ja die Berechnung immer darauf abzielt,
vaablen Bindungen zu erzeugen, ist hier die,
das Ziel, Funktionswerte zu erzeugen oder auszurechnen.
Mit gegebenen Argumenten, die daraus resultierenden Funktionswerte zu ermitteln,
weil es eben inhärent im Verarbeitungsmodell der funktionalen Programmierung ist,
dass ich Argumente nur benutzen kann, um Informationen in einen Berechnungsprozess hineinzugeben.
In der Logikprogrammierung wegen der Richtungsunabhängigkeit können wir es auch benutzen,
um Berechnungsergebnisse aus dem Prozess herauszubekommen. Dieser Weg ist uns verschlossen,
die einzige Möglichkeit, Ergebnisse aus dem Berechnungsprozess in Erfahrung zu bringen,
über Nebeneffekte geht es natürlich auch,
aber im Kern der funktionalen Programmierung ist der einzige Weg über den Funktionswert an das Berechnungsergebnis zu bekommen.
Wir machen das Ganze wie auch das,
was Frau Tretchlight parallel dazu macht, an der Hand von Scheme,
eine einigermaßen standardisierte Sprache, die relativ einfach gehalten ist.
Das kommt uns sehr entgegen. Wir werden sie noch
ein Stückchen weiter vereinfachen, um nicht zu viele technische Probleme
mit ins Spiel
zu bekommen
Der Standard, das ist dieser Revised Report, lässt aber
sehr viele Einzelheiten unspezifiziert und deshalb gibt es also verschiedenste Systeme mit Implementationsunterschieden.
Im Rechenzentrum haben sie zur Verfügung,
wenn sie es benutzen wollen, dass MITs geben.
Das ist einer der Urväter der Scream-Entwicklung und etwas moderner,
das System Racket, was früher Dr. Scream hieß.
Die Syntax, die wir verwenden, leicht vereinfacht, alles,
was wir weglassen, sind die Makros und reservierte Symbole für Quote und Backquote.
Wir müssen also Quote und Backquote, kommen wir noch dazu,
was das ist, immer als,
das ist nur für die Insider,
immer als Funktionen schreiben
Das heißt, wir können nicht irgendwie mit dem Quotesymbol tatsächlich arbeiten,
sondern müssen tatsächlich, Klammer auf,
Quote und dann kommt der Ausdruck dequotiert werden soll, schreiben.
Die Syntax sagt uns ein Ausdruck, kann im Prinzip nur drei Sachen sein.
Es ist entweder eine konstante, ein Name oder ein S-Ausdruck.
Eine Konstante kann nur eine Zahl und ein Wahrheitswert sein.
Also in dem Sinne, Atome,
wie wir sie im Prolog haben, gibt es hier nicht.
Das, was im Produkt ein Atom ist,
ist hier immer ein Name. Und ein Name ist immer
eine vaiablen Bezeichnung und eine vaiablen bezeichnet wird immer ausgewertet,
wenn sie die Auswertungsprozedur hineinkommt.
Es sei denn,
ich verhindere es explizit
In ein Produkt müsste ich dafür sorgen, explizit,
dass ich, na, okay,
wir unterscheiden da zwei verschiedene Sorten von Namen,
die mit den Großgeschriebenen Anfangsbuchstaben sind,
die Variablen und die anderen Sonstigen sind sonstige Atome.
Diesen Unterschied gibt es hier nicht. Wahrheitswerte sind also True und False,
die werden immer mit dem Schweinegatter geschrieben, Variablen sind eben Namen.
Und dann gibt es sogenannte S-Ausdrücke. Und S-Ausdrücke sind entweder
Listen oder gepunktete Paare und das ist das erste Problem,
in das wir laufen,
Listen sind also immer gekennzeichnet,
sind taktisch durch öffnende und schließende
runde Klammer
Und dazwischen eine Aufzählung von Elementen, beliebig viele. Die,
und das ist das Wesentliche, durch Leerzeichen getrennt sind.
Eingepunkte, es ist Paar hingegen,
ist eben ein Paar mit öffnender und schließender Klammer.
Ein Punkt in der Mitte und einem vorderen Element und einem hinteren Element.
So viel kann ich schon vorwegnehmen. Dies ist dieser Punkt hier,
ist Äquivalent zu der Syntax des senkrechten Striches in Prolog.
Das ist exakt dasselbe. Gut, damit haben wir die Syntax,
die kriegen wir implizit immer noch mal wieder rein.
Das müssen wir jetzt nicht weiter diskutieren. Interessant,
da ist diese Mantik, denn da wollen wir ja hin.
Wir wollen ja Interpreter schreiben, der uns die funktionale Semantik
für diese Form von Ausdrücken realisiert.
Wir fangen natürlich immer ganz einfach an und werden dann zunehmend komplexer.
Konstanten haben sich immer selbst als Wert. Wir hatten ja konstant
nur als Zahlen und Wahrheitswerte, andere Konstanten gibt es nicht.
Und die werden immer zu sich selbst aus.
Also wenn ich in das System eine 2 reingebe,
dann kriege ich auch eine 2 wieder raus,
weil das genau der Wert dieser Konstanten ist.
Oder das Gleiche eben für Wahrheitswerte
Das zweite waren Vayablen, bei Jablen sind also Namen und ich kann diesen Vayablen einen Wert zuweisen.
Zum Beispiel die Standard-Vayante ist, ich nehme diese Funktion de fein dafür.
Wenn ich also einen Namen in das System eingebe,
das ist ein Name, dann kriege ich einen Fehler,
weil dieser Name keinen Wert hat.
Das ist die Fehlernachricht, die ich bekomme.
Ich kann jetzt am prompt des Systems eingeben,
definiere mir X zu 2 oder weise der Vayablen X,
den Wert 2 zu, dann kriege ich den Wert dieses Ausdrucks,
der ist nirgendwo spezifiziert in den Standarddokumenten, das heißt,
das kann jeder Implementator so machen,
wie er es möchte
Ich weiß nicht, was Reckett sagt, aber MIT-Scheme sagt,
glaube ich, Undefined an der Stelle.
Aber das, bitte? Wer? Procedures. Aha.
Was soll das bedeuten? Nö, das ist mit Sicherheit
nicht. Das hat ja auch keine Funktionsdefinition. Also gut,
jetzt müsste ich, bin ich jetzt überfragt,
weiß ich nicht, muss man ausprobieren.
Heißt aber jetzt, ab diesem Zeitpunkt hat die Vayable X den Wert 2.
Und wenn ich jetzt den Wert von X wieder abfrage,
kriege ich tatsächlich den Wert dieser Vayablen zurückgegeben
Die dritte Variante S-Ausdrücke, da muss ich den Wert tatsächlich berechnen.
Das sind ja doch keine Berechnungsprozesse, die ich gemacht habe.
Das ist ja nur den Wert einer konstanten Abfragen oder eben
einer Variablen einen Wert zu weisen und dann diesen Wert,
den ich einmal zugewiesen habe, wieder zurückzuholen.
Der interessante Teil sind die S-Ausdrücke. Dort muss ich wirklich eine Berechnung anstoßen.
Und die Berechnung ist, im Standardfall eine funktionale Applikation.
Es gibt aber noch verschiedene Varianten mehr. So,
und jetzt fangen wir an, unseren Interpreter zu schreiben.
Der Kern des Ganzen ist eine dreistellige Spredikat,
was eine Auswertung einer funktionalen Ausdrucks vornimmt
Und wir nehmen die Fälle, die wir bisher betrachtet haben,
die schreiben wir jetzt hier rein. Also ein Ausdruck wertet
zu sich selbst aus, wenn er denn eine Zahl ist.
Das ist ja dieser Test hier. Der sagt,
das Ding ist eine Zahl. Das heißt, was immer
ich hier eingebe, kriege ich hier auch wieder heraus.
Das dritte Argument hier oder das zweite in der Mitte dazwischen ist eine sogenannte Umgebung,
was eine Umgebung genau bedeutet, kriegen wir noch später.
Diese Definition sagen nur, das gilt in jeder Umgebung.
Es ist egal, wo ich das mache,
in jeder Umgebung gilt, wenn der Ausdruck eine Zahl ist,
dann wertet er zu sich selbst aus
Und das Gleiche eben für die Wahrheitswerte, True,
wertet in jeder Umgebung zu True aus und Falls wertet in jeder Umgebung zu Falls aus.
Das Ganze ist wieder ein Fall von Defaultschließen. Wann immer
eine dieser Klauseln erfolgreich war, muss ich mir die anderen nicht mehr angucken.
Gut, und das können wir jetzt testen,
jetzt fangen wir ja, dieses Evil ist unser Scream-Interpreter.
Jetzt können wir den loslegen und sagen,
wir geben in den Interpreter eine 2 hinein und Heureka,
wir kriegen eine 2 heraus.
Na, das funktioniert ja bestens. Wir geben ein Truh
hinein und wir kriegen ein Truh heraus. Das ist,
die Arbeit ist getan, wir sind fertig,
jetzt wird es komplizierter.
Das erste Problem ist die Syntax
Die für die Listen oder die S-Ausdrücke, hatte ich ja gesagt,
die Syntax ist, da oben ist es angedeutet,
immer öffnende Runde, Klammer, schließende Runde,
Klammer und dazwischen eine Aufzählung von Elementen, getrennt durch Leerzeichen.
Das ist eine traditionelle Konvention, die kommt aus der Liz-Familie
und ist auf einen historischen Unfall zurückzuführen.
Eine der erfrühen Liz-Implementationen hatte an diesen Stellen hier dazwischen immer noch,
wie in Prolog ja auch, Kommas zu stehen.
Nur leider Gottes war es ein Programmierfehler und das Ganze ist
auf ein nicht druckbares Zeichen abgebildet worden. Das heißt,
im Input über die Lochkarten wurde immer noch fleißig von Kommas eingegeben,
im Output standen die Kommas nicht mehr drin,
weil sie nicht druckbar waren.
Und der Output hat den Leuten aber so gut gefallen,
dass sie bei der Konvention geblieben sind.
Sie haben Leerzeichen als Trennsymbol für die Listenelemente benutzt.
Und damit ist das Leerzeichen so semantisch überladen. Es ist nämlich einerseits,
dass der Separator für die Listenelemente, was in Produkt das Komma ist,
es ist aber andererseits auch das Filmmaterial zwischen den Syntax-Elementen.
Denn ich kann beliebig viele Leerzeichen dort einfügen und
nicht jedes dieser Leerzeichen ist ein Trennelement
Also wenn ich ein Leerzeichen sehe, weiß ich erst mal nicht so ohne weiteres,
ist es jetzt der Separator oder ist es einfach nur ein Füllelement?
Weshalb ich um technische Probleme zu vermeiden,
einen Trick angewendet habe, dass ich ein Spezial oder das,
was ich Peace geben nenne,
weil sie Polog syntags für Scream verwendet,
dass ich nämlich die Listeninterpretation oder die Listennotation von Prolog benutze,
auch für die Scream-Programme.
Also im Weiteren werden wir Scream-Listen so schreiben, im Original würden sie so aussehen.
Und das Gleiche gilt eben für gepunktete Paare,
im Original bei Scream sehen sie so aus,
wir schreiben sie so,
wie wir sie von Prolog her gewöhnt sind
Ansonsten ändert sich nicht viel. Die Zahlen sind natürlich genauso.
Die kleingeschriebenen Namen sind genauso. Und wir müssen eben jetzt
nur die großgeschriebenen Namen von Scream auf kleingeschriebenen Namen in Prolog abbilden,
sonst kriegen wir, sonst würden wir die vajablen Interpretationen in Prolog bekommen.
Die wollen wir natürlich nicht. Wir wollen die tatsächlich als Name haben,
wo wir wissen, es ist eine Variable,
aber es ist keine Prolog-Variable.
Es ist eine Scream-Variable und den Unterschied müssen wir sauber aufrechterhalten.
Ich umgehe das Problem dadurch, dass ich keine
großgeschriebenen Namen verwende
Dadurch entfällt das Problem. So, damit haben wir also Vayablen,
den Bayablenbe, sind wir beim Vayablenbegriff. Also ein Name,
der steht für eine Vayable und ich kann diesen Vayablen
abstrakte Objekte zuweisen und diese abstrakten Objekte können alles sein.
Es gibt da keine Einschränkungen. Es können also auch wieder Funktionen sein,
die ich einem Namen zuweise. Da gibt es keinen Unterschied dazwischen,
ob das eine konstant ist, ob das eine Funktion ist,
ob das eine Liste ist, das ist alles dasselbe.
Und das macht die Sprache so schön einfach, weil ich
eben keine prinzipiellen Unterschiede zwischen diesem Objekt Jahr Objekten habe.
Benennung über die Variable ist also wieder eine einfache Form von Abstraktion.
Ich kann, wenn ich eine Variable abfrage, nicht mehr erkennen,
wie dieser Wert zustande gekommen ist, in die Variable hat.
Ich kann also der Variablen, das ist dieser Name hier,
entweder eine Konstante zuweisen, wie wir das eben gemacht hatten.
Oder ich kann dieser Variablen eine S-Expression zuweisen. Und ja,
diese gut, das machen wir später. Dass die fein selber ist,
keine Funktion, sondern eine sogenannte Special Form Expression.
Wir werden jetzt weiteren unterscheiden müssen zwischen Funktionen einerseits,
die wir auch selber definieren können und Special Form Expressions,
die typischerweise vordefiniert sind durch das Systems.
Der Wert ist die feinen Ausdrucks, ist auch undefiniert,
das hatten wir schon gesehen. Da kommt also irgendetwas raus,
das ist für das Verhalten des Systems völlig irrelevant.
Gut, im Prinzip ist funktionale Programmierung ja gedacht,
als ein deklarativer Berechnungsformalismus und damit er deklarativ ist,
darf er keine Wertemanipulation unterstützen
Im Prinzip tut das Scream auch, lässt aber zahlreiche Hintertüren auf.
Also das, was Prolog überhaupt nicht kennt,
dass wir hingehen und irgendwelche Variablen an irgendwelche vajablen Werte ändern können,
das ist ja ein Privileg des Prolog-Systems.
Das ist in Scream tatsächlich möglich. Ich kann also mithilfe von Define
oder auch mithilfe von der Set-Familie existierende Werte einer vajablen ändern.
Und auf die Art und Weise ist die Hintertür zur Nebeneffekt
behafteten Programmierung geöffnet. Weil das so ist,
werden solche Sachen auch üblicherweise gekennzeichnet mit einem syntaktischen Marker,
der dann als Bang gesprochen wird.
Dieses Set Bang heißt also, ja,
ich setze eine Variable auf einen Wert,
aber es ist eben eine Nebeneffektbehaftete Operation.
Ich kann damit möglicherweise einen alten Wert überschreiben und ist insofern zustandsändernd für die Maschine.
Wir sind also bei solchen Wertezuweisungen jetzt im Bereich der interoperativen Sprachen angekommen.
Das ist leider ein Defekt, den die funktionale Programmierung hat.
Da ist die Logikprogrammierung radikaler, wo sie sagt,
das ist Privileg des Systems,
da kommst du als Programmierer
überhaupt nicht ran
Es gibt noch eine weitere Reihe von Einschränkungen für Namen,
die dürfen nicht doppelt belegt werden.
Also ich darf nicht zum Beispiel eine der Standardfunktionen,
die das System kennt, also Square Root oder Sinus oder Apent,
das darf ich nicht nochmal als vajablen Namen verwenden,
dann kommt zu einer Kollision.
Und das gilt eben auch für der syntaktische Schlüsselwörter,
das sind typischerweise die Special-Formen-Expression-Namen,
die dürfen auch nicht all zwei jablen Namen benutzt werden.
Gleichermaßen die Konstanten auch, also die logischen Konstanten. Und weil,
ach, das gleiche gilt für die Interpretationszeichen, Klammern,
Quotes und Backquotes dürfen nicht Teil des Namens sein,
das ist auch reserviert
Gut, dann gibt es verschiedene Arten von S-Ausdrücken,
das hatte ich schon angedeutet, der typische ist der Funktionsaufruf,
ein Funktionsaufruf ist also immer eine Liste,
die beginnt mit einem Funktionsnamen und gefolgt von einer Liste von Argumenten.
Dieser Aufruf sagt also,
ich bin die Funktion Square Root und ich habe ein Argument,
das fängt hier an,
das fängt hier an und das hört hier auf
Dieses Argument ist wieder ein Funktionsaufruf, nämlich die Addition.
Mit zwei Argumenten, das erste Argument ist dieser Klammerausdruck und
das zweite Argument ist dieser Klammerausdruck. Dieses Argument ist wieder
ein Funktionsaufruf, nämlich in diesem Fall von der Exponentialexponentialfunktion.
Also nimm dieses X hier und setze es zum Quadrat,
sodass im Endeffekt herauskommt, Ja, nichts herauskommt,
weil wir die Werte von X und Y nicht kennen.
Das gleiche Ding jetzt in unserer Prolognotation geschrieben, sieht eben so aus,
dass ich runde Klammern durch eckige Klammern ersetze und dass ich
die Trennelemente zwischen Listenelementen durch Kommas explizit mache.
Das ist also eine 3, sorry,
eine 2-elementige Liste und das sehen wir hier auch,
hier ist das erste Element, hier ist das zweite Element.
Das ist wieder eine Liste mit diesmal 3 Elementen, das ist das erste,
das ist das zweite und das ist das dritte Element.
Also das ist diese interne Notations, äh,
diese prolog angelehnte Notationsweise. Eine spezielle Form von,
ähm, oder eine andere Form von S-Ausdrücken sind Lamdeausdrücke,
Lamdeausdrücke sind anonyme Funktionsdefinitionen,
so wie ich das vorhin in dem Teaser auch drin hatte,
die sind gekennzeichnet durch diesen, durch dieses Stichwort Lamda.
Das heißt, was jetzt kommt, ist eine Funktionsdefinition.
Ich habe eine Liste von Argumenten,
die ich angeben kann und ich habe eine Liste von S-Ausdrücken.
Und die Semantik eines solchen Lamdeausdrückes ist es, Wende,
diese Liste von S ausdrücken, auf diese Argumentbelegungen an,
Und der Funktionswert ist der Wert des letzten S-Ausdruckes in dieser Liste hier hinten.
Also, ich kann dort mehrere S-Ausdrücke in Folge haben,
nur der Wert des Letzten ist für das Berechnungsergebnis tatsächlich relevant.
Heißt auf gut Deutsch nichts anderes,
in der funktionalen Programmierung macht es nur Sinn,
in der reinen funktionalen Programmierung, Körper von Funktionen zu verwenden,
die einen einzigen S-Ausdruck haben.
Weil nämlich die Werte der Vorangegangenen
als Ausdrücke verloren sind
Es sei denn, ich arbeite mit Nebeneffekten.
Dann verlasse ich aber die reine Welt der funktionalen Programmierung.
Wenn ich mit Nebeneffekten arbeite, dann macht es natürlich Sinn,
zum Beispiel fünf Printbefehle hintereinander zu schreiben.
Warum auch nicht? Dann bin ich ja nicht am Wert
dieser Printbefehle interessiert. Der ist ja vermutlich wieder vollkommen undefiniert,
sondern nur an dem Nebeneffekt, den sie erzeugen.
Gut, also eine solche Funktionsdefinition könnte jetzt eben so aussehen,
dass sie dieses Stichwort Lamm da hier vorn als erstes Element hat.
Dann kommt die Liste der Argumente,
ich bin eine Funktion von zwei Argumenten
Und ich bin meine Berechnungsvorschrift, ist dieser S-Ausdruck hier hinten,
der sagt, bilde die Quadratwurzel aus der Summe von X,
Quadrat und Y-Quadrat.
Also nichts anderes als ein versteckter Pythagoras, der hier drin steht.
Quadriere X, quadriere Y, summiere die beiden Funktionswerte
und wende die Quadratwurzel zum Schluss da drauf ein.
Und das Gleiche wieder in unserer Prolog-inspirierten Notation mit den expliziten Kommaseparatoren drin.
Das ist nur, damit man sich an die Notation
ein bisschen gewöhnt
Die dritte Form von S-Ausdrücken sind sogenannte Special Form Expressions.
Und Special Form Expressions, definieren typischerweise eine spezielle Abarbeitungsreihenfolge,
die von der Standardabarbeitungsreihenfolge abweicht.
Da kriegen wir dann noch Beispiele zu sehen.
Eins haben wir schon gesehen, das ist das die Fein.
Dass die Fein ist, ein Spess-Special Form Expression,
weiß jemand warum? Er ist wahrscheinlich dumm zu fragen.
Alle haben bei Frau Dreaddreschleiter schon... Ist jemand
noch nicht in den Genuss der funktionalen Programmierung gekommen
Oh ja, ich habe das, das hätte ich nicht gedacht.
Immerhin. Fünf Kunden. Ja, gut. Also,
was ist, warum ist die Fallen,
oder muss die Fallen eines Special Form Expression sein?
Wer die Variante komponente angeht, darf man nicht gleich ausgewertet werden.
Ja, wo ist es besonders kritisch? Das haben wir
jetzt noch nicht behandelt, aber kommt gleich.
Also bei Zahlen wäre es vollkommen egal.
Da kann ich eben auswerten
Das heißt, es ist ja, ja, was,
das Wesentliche ist ja nicht, dass nicht ausgewertet wird,
sondern das selektiv ausgewertet wird.
Was ist das, die fein? Oh, jetzt bin
ich selber. Bitte die Steam-Experten an die Freundin.
Wertet ihr, dass die fein sein zweites Argument aus?
Ich kann linsen. Es steht auf irgendeiner der nächsten Folien
da drauf, aber ich bin jetzt nicht sicher.
Ich bin der Meinung, es wird, das zweite Argument
auf und weist dann den Wert dieser Welt an auch eine Funktion sein.
Ja, gut, also das ist auch das, was ich mir gedacht hätte.
Es ist selektiv. Das erste Argument darf nicht ausgewertet werden.
Denn wenn ich so etwas mache, wie das die Pfeilen,
was wir vorhin hatten, na, wo hatten wir es denn?
Hier hatten wir es. Wenn ich so etwas mache,
wie setze die Valable X auf 2,
dann darf dieses X natürlich nicht ausgewertet werden,
sondern ich möchte hier den Namen der Valablen angeben können.
Wenn das ausgewertet würde, dann würde ich dem Wert dieser Valablen,
Es muss dann wieder ein vaiablen Name sein, etwas zuweisen.
Das will ich natürlich nicht. Sondern ich will der Vayabin selber den Wert zuweisen.
Das heißt, dieser Name hier darf nicht ausgewertet werden,
denn sonst würde ich nicht den Namen bekommen, sondern den Wert des Namens.
Und die Zahl hier hinten, die werdet eh zu sich
selbst aus und tatsächlich hier hinten sollte ausgewertet werden können.
Das heißt also, ich setze dann einen bestimmten Namen auf einen Wert,
den ich hier berechne. Das ist also eine Special-Form Expression,
die nicht alle Argumente auswertet, sondern nur das zweite Argument.
Das erste Argument muss vor Auswertung geschützt werden. Und um das zu schützen,
muss ich es als Ausnahmefall zur Standard funktionalen Anwendung definieren.
Und das sind genau Special Form Expressions.
Aber dazu kommen wir noch im Detail. Das ist sozusagen
eine Vorwettnahme. Gut, wir bleiben erst beim Standardfall.
Das sind die Funktionen, Funktionen, Funktionsdefinitionen sind ja Abstraktion.
Ich habe ja in der Logikprogrammierung den Begriff der relationalen Abstraktion
nur vom Begriff der funktionalen Abstraktion übernommen. Funktionale Abstraktion heißt ja,
ich versuche, einen systematischen Zusammenhang vieler Beispiele,
zum Beispiel die Berechnung der Quadratwurzel, statt durch Einzelbeispiele,
durch eine Berechnungsvorschrift zu definieren.
Das ist die Abstraktion vom Einzelbeispiel hin zu einer generellen Beschreibung eines funktionalen Zusammenhangs.
Insofern sind Funktionsdefinitionen, genauso wie Prädikatsdefinitionen eben immer Abstraktionen.
Die Funktionen müssen keinen Namen haben, das haben wir schon gesehen,
wir können Funktionen als Landerausdruck aufschreiben, das ist eine Funktion,
nur hat sie keinen Namen,
aber wir können einer Funktion jederzeit einen Namen geben und das ist eigentlich auch in vielen Fällen der Regelfall.
Wir werden die Beispiele noch sehen,
wo wir keinen Namen vergeben müssen,
wo es sinnvoller ist als mit einer anonymen Funktion
zu arbeiten
Der Name kann deshalb wegfallen, weil er eigentlich nur drei Zwecken dient.
Entweder der Dokumentation, wenn also Menschen sich über Funktionen unterhalten wollen,
es ist gut, wenn die Dinge einen Namen haben.
Es dient der Wiederverwendung von Funktionen, nur Funktionen,
die einen Namen haben, kann ich jemals in unterschiedlichen Anwendungskontexten wieder benutzen.
Und ich brauche es für den rekursiven Aufruf.
Wann immer ich eine rekursive Funktion definiere,
brauche ich natürlich einen Namen, um deutlich zu machen,
dass sie sich selbst aufruft.
Und dass dieser Selbstaufruf muss ja
durch einen Identifier hergestellt werden
Und natürlich brauchen wir all diese Fälle,
deshalb steht das nur auch in Anführungsstrichelchen,
tauchen all diese Fälle in der praktischen Programmierung auf und deshalb
sind benannte Funktionen natürlich sehr nützlich.
Aber wir werden sehen, dass wir auch Wegwerffunktionen verwenden können,
die wir nur für die einmalige Verwendung definieren und danach wird sie,
wenn sie nie wieder benutzt.
Und das ist sozusagen sinnlos, wäre den Namensraum damit voll
zu müllen und dann können wir auch anonyme Funktionen verwenden.
Das ist einer der wesentlichen Unterschiede zur Logikprogrammierung.
Anonyme Prädikate haben wir dicht in der Logikprogrammierung.
Ganz einfach aus dem einfachen Grunde,
weil ich Prädikate nur abarbeiten kann,
wenn sie in der Datenbank des Prologsystems stehen
Und um sie dort zu finden, brauchen sie einen Namen.
Ansonsten kann ich sie da nicht finden. Das ist ein
zentraler Unterschied. Gut, also ein nackter Lamdeausdruck ist eben
eine unbenannte Funktion oder eben auch eine anonyme Funktion.
Ja, ich kann eben einem Lamdeausdruck einen Namen verpassen und
das passiert eben genau wieder mit der Define mit dem Define genauso.
Das ist eine Wertezuweisung wie jede andere Wertezuweisung.
Ob ich einem Namen eine Zahl zuweise,
ob ich einem Namen einen Wahrheitswert zuweise oder eine der Funktionsdefinitionen,
das macht keinen Unterschied
Ich kann also das klassische Defeind verwenden,
indem ich jetzt diesen Namen hier, den nenne ich Pythagoras,
unseren Lamdeausdruck für die Berechnung der Hypotenusenlänge eingebe.
Also wieder einem X-Quadrat, einem Y-Quadrat,
addiere die beiden und zu ihr die Wurzel da draus.
Damit hat ab sofort dieser Name den Wert dieses Lamdeausdrucks,
Lamdeausdrücke werten auch zu sich selbst aus.
Und ich kann jetzt diese Funktion über diese Namen ansprechen
Also der Wert dieser Variablen ist jetzt diese Funktion.
Und damit kann ich die Funktion als Wert dieser Variablen
in die Berechnung, in eine Berechnungsprozedur mit einbringen.
Weil das sehr häufig gebraucht wird und weil das mit dem Lamm
da da immer ein bisschen mühselig ist, zu schreiben,
gibt es eine Kurzversion dafür, die ohne das Lamm da auskommt.
Und die sieht eben so aus, dass sich
das Aufrufpattern für die Funktion hier als zweites Argument angebe.
Ich habe also wieder die Fein,
aber ich gebe jetzt hier eben nicht nur den Variablen Namen an,
sondern das gesamte Pattern bestehend aus Funktionsname und der Liste der Argumente.
Also sozusagen eine Zusammenfassung von diesem Namen plus dieser Argumentliste.
Das ist dieses Aufrufpattern hier
Und der Körper der Funktion ist natürlich genau wieder der gleiche wie hier oben.
Das sind also alternative Möglichkeiten, die beide bestehen,
um einer Variablen ein Landerausdruck zuzuweisen. Gut,
auf die Art und Weise sind wir jetzt also in der Lage erstmalig,
Funktionen zu definieren und in Mithilfe dieser Funktion Berechnungen vorzunehmen.
Und dazu brauchen wir die Funktionsauswertung,
die in einem Zyklus besteht, oder aus zwei Schritten besteht,
einerseits immer dem Evil-Schritt und andererseits Evel-Schritt und andererseits dem Apply-Schritt
Das Auswerten der Listenelemente, und das ist etwas,
was zur Einfachheit von Steam beiträgt. Im ersten Schritt werden
alle Listenelemente eines Funktionsaufrufs ausgewertet. Wenn ich also einen solchen Aufruf habe,
definiere die Funktion Quadrat zur Produkt einer des Arguments mit sich selbst.
Dann werden alle diese Argumente als allererstes ausgewertet, dass,
achso, Toy, dass die Definition, hier ist der Aufruf,
Ja, ich habe jetzt hier eine Funktion definiert,
berechne mir das Quadrat einer Zahl.
Und hier, wende ich das jetzt tatsächlich an.
Ich habe also hier den Funktionsaufruf und ich gebe ihm hier
ein Argument, denn Square ist eine Funktion eines Arguments.
Dieses Argument ist wieder ein Funktionsaufruf. Anders als diese Square,
was keine eingebaute Funktion ist, ist dieses Plus für die Addition eine eingebaute Funktion.
Und jetzt geht das System also hin und werdet alle Listenelemente,
die es hier bekommt, aus.
Der Wert dieses ersten Listenelements, ja, das ist der,
den wir hier gerade zugewiesen haben,
nämlich ein Landerausdruck
Das ist der Lamdeausdruck. Ich bin eine Funktion eines Arguments und ich soll
mein Argument mit sich selbst multiplizieren, um den Funktionswert zu bekommen.
Das ist der Wert dieses ersten Listen-Elements.
Und genauso muss ich den Wert des zweiten Listen-Elements berechnen,
das ist eben eine eingebaute Funktion, die sagt,
addiere diese Argumente hier und dann kommt eben sechs hier heraus.
Jetzt habe ich hier einen Lamdeausdruck, also eine Funktion,
die ich auf diesen Funktionswert anwenden kann.
Ich binde die Vayable X an dem Wert sechs,
ich multipliziere die sechs mit sich selbst
und habe den Funktionswert dieses Aufrufes mit 636 ermittelt
Also immer zuerst auswerten aller Listenelemente,
wenn ich alle Werte habe, dann kann ich die Funktion,
die ich hier vorne, hiervon muss eine Funktion stehen.
Also es muss eine Funktion als Wert herausbekommen.
Dann kann ich die Funktion, die ich hier vorne als
Wert ermittelt habe, auf die restlichen Argumente anwenden.
Das ist immer das Grundschema, jeder Funktionsanwendung ins Keep.
Wert aller Listenelemente aus, nimm den Wert des ersten Listenelements,
was eine Funktionsdefinition sein muss und wende es
auf die restlichen Listenelemente als Argumente an
So, und das müssen wir umsetzen. Da kommt jetzt aber
der Begriff der Umgebung herein. Eine solche Auswertung wird immer
in bestimmten Umgebungen passieren. Und die Umgebungen sind Gültigkeitsbereiche für Variable.
Gültigkeitsbereiche für Variable hatten wir in der Logikprogrammierung nicht.
Oder wir hatten sie in einer sehr trivialen Form.
Was sich in dem Grundsatz widerspiegelt, der Gültigkeitsbereich der Variablen ist die Klausel.
Wann immer innerhalb einer Klausel eine Variable steht,
dann ist es die gleiche,
oder mehrere Vorkommen der gleichen Variablen passieren,
ist es die gleiche Variable,
wann immer dies in unterschiedlichen Klauseln passiert,
sind es
unterschiedliche Variablen
Deshalb immer der Grund, dass wir allen Variablen am Anfang umbenennen,
um Konflikte zwischen unterschiedlichen Klauseln von vornherein zu vermeiden.
Hier können solche Konflikte tatsächlich auftreten. Ich kann also in
unterschiedlich eingeschachtelten Funktionsdefinitionen durchaus die gleichen Variablen verwenden und dann tritt ein Effekt auf,
dass die inneren Variablen, also die rekursiv eingebetteten,
die äußeren Variablen verschatten, dass sie nicht mehr sichtbar sind für die Verarbeitung.
Ich muss also in jedem Falle immer ermitteln,
Welche dieser Bayablen ist jetzt eigentlich die gültige?
Wenn da irgendwo ein X steht, dann muss ich dafür sorgen,
rauszubekommen, was genau dieses, welche Variante dieses Xs jetzt genau verwendet wird.
Und das wird durch die Umgebungen geregelt. Ich muss also immer mitführen,
in welcher Umgebung mache ich eine bestimmte Auswertung zurzeit,
weil eben Umgebungen charakterisiert sind, durch die Variablen,
die in dieser Umgebung gültig sind.
Über die Umgebung weiß ich, was sind die jetzt,
hier gültigen Variablen, wenn also mehrfach die Variable X irgendwo auftaucht,
weiß ich über die Umgebungsdefinition,
welches das zugehörige X tatsächlich ist
Und ich muss immer dafür sorgen, rauszubekommen,
was ist eigentlich die ERC jeweils gültige Variable als Entwickler,
als Softwareentwickler, indem ich Buttom Up nach einer Variablenbindung suche.
Das ist also der Hintergrund der Umgebungen.
Wir gucken uns dann gleich noch Beispiele an.
Wir können in Umgebungen in jeder Funktionsaufruf macht standardmäßig eine Umgebung
auf und die Argumente der Funktionen sind Variablen in der betreffenden Umgebung.
Wir können zusätzlich zu den Argumenten der Funktion auch noch weitere Variablen einführen,
die nicht als formale Parameter in der Signatur der Funktion auftauchen.
Dafür gibt es wieder eine Special Form Expression, die heißt Lett.
Und die enthält immer Name Wertpaare, die also bestimmte Namen
als Variablen einführen und ihnen jeweils gleich einen Wert zuweisen.
Und das können beliebig viele sein und zum Schluss kann wieder ein Funktionsaufruf kommen,
der also unter Verwendung dieser lokalen vajablen Bezeichner und ihrer Werte
eine bestimmte Berechnung vornimmt
Es gibt eine einzige Ausnahme,
wenn er hier irgendwo einen Bezug zwischen diesem Name Wertpaaren passiert.
Also wenn ich zur Berechnung dieses Wertes hier einen Wert brauche,
der gerade vorher definiert worden ist, dann muss ich Letztstern verwenden.
Da, wenn solche inneren Abhängigkeiten existieren.
Hier also ein Beispiel für die Verschattung.
Ich bin erst mal auf der globalen Ebene des Systems.
Ich definiere eine Variable X und gebe Ellenwert 3.
Ich definiere eine Variable Y und gebe ihr den Wert 4.
Ich definiere eine Variable Pythagoras und gebe ihr als Wert
diese Funktionsdefinition
Diese Funktionsdefinition hat intern wieder noch zusätzliche Varianten.
Also sie hat die Variablen XY und für zusätzlich noch
die Variablen Square X2 und Y2 ein.
Dieses Let macht auch wieder eine extra Umgebung auf.
Diese Variablen Square X2 und Y2 sind jetzt nur in der Umgebung dieser Let-Umgebung tatsächlich sichtbar.
Und sodass wir insgesamt auf ein mehrfach geschachteltes System kommen.
Wir kamen einerseits die globalen Variablen hier draußen.
Das ist unsere Funktion Pythagoras und das sind die
X und Y-Parameter
Dann haben wir die Umgebung von Pythagoras, die erst aufgemacht wird,
in dem Augenblick, wo die Funktion Pythagoras aufgerufen wird.
Umgebungen entstehen nicht durch die Definitionen der Funktion,
sondern durch den Aufruf der Funktion.
Das ist wesentlich. Also in dem Augenblick,
wo ich die Funktion Pythagoras aufrufe hier hinten,
wird eine neue Umgebung aufgemacht und die hat die lokalen Variablen der Funktion Pythagoras.
Die macht jetzt die Latt-Umgebung auf und in der Latt-Umgebung
wären jetzt Square X2 und Y2 als neue Werte eingeführt.
Das ist also
die innere Umgebung hier
Gut, also wir machen erst diese drei Definitionen und anschließend rufen wir den Pythagoras,
die Funktion Pythagoras auf,
wir lassen das erste Argument als Addition des Wertes von X mit 3
und das zweite Argument als Addition von Y mit 2 berechnen und wollen dann die Hypottenuse eines Dreiecks wissen, was entsprechende Kathetenlängen hat.
Das ist nur wieder die Variante in Peace-Geam-Notation,
wo also die Prolog-Listen-Konvention verwendet wird und Da sieht man schon,
es gab gute Gründe, das hier einfacher zu finden als jenes.
Auf der anderen Seite ist eben hier, naja,
das ist Geschmackssache, aber die Klammerstruktur ist schon eine erhebliche Herausforderung rauszukriegen,
wo immer die jeweils öffnenden und schließenden Klammern sind,
sodass es also empfehlenswert ist, sich eine Notation zu überlegen,
wo das einigermaßen das Klammergleichgewicht einigermaßen verdeutlicht wird.
So, ich musste auch hier wieder noch Kompromisse machen,
den Star habe ich also ausgeschrieben,
damit ich nicht in Konflikte komme
Also, der Gang der Dinge ist jetzt,
zuerst wird dieser Ausdruck ausgewertet, in der globalen Umgebung
wird die Variable X mit dem Wert 3 eingeführt.
Dann wird hier die Variable Y mit dem Wert 4
eingeführt und die Variable Pythagoras mit dem Lamda-Ausdruck als Wert.
Und zwar der gesamte Lamda-Ausdruck außerhalb des Letzten. Dann wird
die Funktion Pythagoras aufgerufen und es wird eine neue Umgebung geöffnet,
die die gleichen Variablen Bezeichner enthält, wie diese globale Umgebung.
Und jetzt sehen wir den Verschattungseffekt innerhalb dieser Pythagoras-Funktion,
nehmen wir natürlich nur diese lokalen Bayablen und nicht die
globalen Variablen hier
Das nennt man Verschattung, dass diese Variablen hier die Außenliegenden überlagern.
Die Außenliegenden sind jetzt nicht mehr sichtbar. Das ist
ein Sichtbarkeitsbereich für eine Variable, der hier definiert wird.
Also eine Umgebung ist genau dieser sichere Sichtbarkeitsbereich.
Innerhalb dieser Umgebung sind X und Y diese beiden formalen Parameter
der Funktionen und nicht diese beiden globalen Parameter.
Jetzt wird durch das Let eine weitere Umgebung aufgemacht mit inneren Variablen.
Ich bin so nett gewesen, sie anders zu benennen.
Ich hätte natürlich brutal sein können und die jetzt auch wieder
X und Y nennen, dann hätten wir wieder einen Verschattungseffekt,
dass die inneren Variablen die äußeren hier verschatten
Ich habe es freundlicherweise mit anderen bariablen Namen gemacht,
sodass der Verschattungseffekt gar nicht auftritt. So,
und jetzt ist das das Wesentliche der funktionalen Programmierung,
dass ich in jedem Falle wissen muss,
über welche Instanz der Variablen eines bestimmten Namens ich eigentlich spreche.
Das heißt, ich muss immer wissen, in welcher der Umgebung ist,
diese bariable definiert worden und was ist ihr Wert in dieser Umgebung?
Ich muss also für jede Auswertungsprozedur immer mitschleppen,
in welcher Umgebung, über welche Umgebung spreche ich jetzt gerade.
Das macht die Berechnung tatsächlich etwas aufwendig,
denn diese Umgebungsverwaltung, die müssen wir jetzt implementieren.
Das ist die unmittelbare Konsequenz davon
Das machen wir, indem wir Fakten in die Datenbasis schreiben.
Wir haben ja eine Datenbank, warum sollen wir die nicht benutzen?
Gemeinerweise habe ich zwei verschiedene Environment-Prädikate. Das eine ist,
die für die rekursive Einbettung zuständig, das andere ist für die Valiablenbindung zuständig.
Erstmal das Dreistellige, das gibt mir die Valiablenbindungen.
Ich sage also hier in einer bestimmten Valiablen,
die ich E11 genannt habe, das ist Fantasienname,
hat die Valiable X den Wert 6.
Und die Valiable in der gleichen Umgebung hat die Valiable Y
auch den Wert 6.
Und in der gleichen Umgebung hat die Valiable Square
den Wert dieses Landerausdrucks
Das sind die vajablen Bindungen in einer bestimmten Umgebung.
Die werden so als Prolog-Fakten in die Datenbank reingeschrieben.
Das heißt aber auch, wenn ich anfange,
eine Funktion auszuwerten, muss ich eine neue Umgebung gründen.
Ich muss ihr einen Namen geben und ich muss alle Vagiablen mit ihren jeweiligen Wertebindungen,
die entweder von draußen in die Funktion reinkommen oder durch ein
Latt-Ausdruck erzeugt werden, die entsprechenden vajablen Bindungen da reinschreiben.
Das ist die Aufgabe, die ich machen muss.
Nun können jetzt solche Umgebungen rekursiv eingebettet werden. Das sehen wir hier.
Diese Umgebung enthält diese als Teilumgebung und wir implementieren es andersrum.
Wir sagen immer, was ist die Subumgebung zu einer gegebenen äußeren Umgebung?
Wir sagen also, E12 ist eine eingebettete Umgebung von E11.
Und E11 ist eine eingebettete Umgebung der globalen Umgebung.
Und wir vereinbaren das Global, ein Bezeichner sein soll für
die oberste Umgebung. Wenn wir also bei Global angekommen sind,
macht es keinen Sinn mehr,
noch weiter nach oben in der Hierarchie zu suchen,
ob es die Variable gibt,
wenn es sie bis dorthin nicht gefunden worden ist,
dann ist sie offensichtlich undefiniert.
Dann muss ich
in einen Fehlerfall laufen
Gut, die Dinger müssen dynamisch veränderbar sein. Denn jetzt muss ich,
wenn ich eine Variable, eine Umgebung aufmache,
muss ich die Eintragung in die Datenbank schreiben.
Und wenn ich die Umgebung wieder zumache,
das heißt also, wenn ich die Abarbeitung beendet habe,
muss ich das alles wieder aus der Umgebung beseitigen.
Das heißt, wir müssen sie als dynamisch erklären und wir müssen dafür sorgen,
dass am Anfang die Datenbank tatsächlich leer ist,
dass da nicht noch Überreste alter Berechnungen drinstehen.
Das muss am Anfang passieren. Und jetzt geht es eben los.
Jetzt müssen wir das Ganze, uns ein paar Methoden schreiben,
um mit einer solchen Datenbank umgehen zu können.
Also das Einfachste ist eben,
gib mir einen Wert einer Variablen in einer bestimmten Umgebung
und binde ihn an die Variable Welle
So, dann gehe ich eben in meine Datenbank und sage,
wenn es denn einen Eintracht gibt mit dieser Umgebung und diesem valiablen Namen,
dann hast du den Wert gefunden.
Dann ist alles okay. Interessant ist es eben,
wenn das nicht der Fall ist,
dass es also der Elzzweig hier unten,
wenn du oben angekommen bist an der,
auf der globalen Ebene und hast, dann,
wenn du noch nicht angekommen bist auf der globalen Ebene,
dann gehe eine Ebene weiter nach oben und gucke,
ob du den
nicht dort diesen Wert finden kannst
Ja, und das ruft sich eben rekursiv wieder auf,
solange, bis es irgendwann mal auf der globalen Ebene angekommen ist.
Und wenn es bis ganz da oben hin kein Wert gefunden hat,
dann kommt dieser Els-Zweig hier zum Tragen,
dann wird ein Fehler geworfen. Dann sagt man eben,
ich habe alle Umgebungen von unten nach oben abgeklappert,
nirgendwo gab es eine vajablen Bindung für diesen Namen,
das heißt offensichtlich ist das Ding undefiniert
Gut, das ist der Einfach, der einfache Teil.
Hier frage ich ja nur ab, aus der Datenbank ist
der Wert drin enthalten oder ist er nicht drin enthalten?
Interessanter ist es also tatsächlich einerseits eine Variable zu erweitern,
also eine neue Subumgebung in einer,
nicht eine Variable, eine Umgebung zu erweitern.
Eine Subumgebung in dieser Umgebung aufzumachen und dann in einer solchen
neuen Umgebung die Werte der Variablen tatsächlich einzutragen.
Um eine neue Subumgebung zu kreieren, muss ich erstmal einen
neuen Namen dafür bilden. Dafür gibt es ein Prädikat,
das nennt sich Gensym, das gibt mir Neue Namen,
ich kreiert mir neue Namen mit diesem Prefix.
Und ich wollte die eben e-something nennen, damit sie für Environment stehen.
Ich kreiere hier einen neuen Namen und anschließend trage ich ein,
dass diese neue Umgebung eine Subumgebung der alten Umgebung ist.
Damit habe ich es in der Hierarchie verankert.
Und ich rufe der Spinnen der Valiablen auf,
indem ich hier die Werte habe, die variablen Namen und die Umgebung,
in der sie eingetragen werden sollen.
Das ist nämlich die neue Umgebung, die ich hier
gerade kreiert habe
Ja, dieses Eintragen der Variablen ist also,
das habe ich so gemacht, eine Liste von Variablen,
eine Liste von Werten und die gehe ich einfach rekursiv parallel durch,
indem ich immer diesen Wert an diese Variable binde,
indem ich diesen dreistelligen Environment-Eintrag in die Datenbasis schreibe.
Ja, also in dieser Umgebung wird diese Variable auf diesen Wert gesetzt.
Ja, und die Umgebung kriege ich von draußen mitgeteilt.
Das ist der Aufruf, hier sagt mir ja,
in welcher Umgebung sind die vaiablen Werte einzutragen.
Also das ist eine kleine Datenbank,
die für ein hierarchisches System von Umgebungen mir Werte verwaltet.
Ja, jetzt müssen wir wieder auf die Scream-Ebene kommen und
die Basis für die verschiedenen Primitive, die wir dort haben, definieren.
Also das erste ist, wir hatten ein Define. Das heißt,
wir müssen jetzt, dass die fein implementieren,
damit unser Scream-Interpreter in Prolog, eine Semantik für eine Funktion
oder in diesem Fall eine Special Form Expressions namens Steve Hain bekommt.
Und die Fein hat eben, ich habe eine Variable,
ich soll ja Variablen einen Wert zuweisen und ich muss das
in einer bestimmten Umgebung machen
Und dann gucke ich eben erst mal, ob die Vagable
möglicherweise schon definiert ist. Wenn sie das ist, muss ich
den alten Wert entfernen. Und nachdem ich ihn entfernt habe,
oder es war vorher gar keiner da,
kann ich eben die neuen Eintragungen in die Datenbank schreiben.
Indem ich denen einfach diesen Eintracht da reinsetze.
Ändern eines Vagablen Werts hat nur ein kleines,
abweichenden Semantik-Teil, nämlich,
dass es einen Fehler wirft,
wenn die Vagable noch
gar nicht existiert
Umgekehrt. Definieren kann ich eine Variable ja natürlich,
wenn sie noch gar nicht existiert.
Set Bang kann ich nur durchführen, wenn die Variable bereits existiert.
Ansonsten funktioniert das Ganze nicht. Das ist tatsächlich, ich habe eine Variable,
die schon einen Wert hat und ich überschreibe den alten Wert.
Das heißt, jetzt muss ein Eintracht in der Datenbank existieren,
was ich hier durchteste. Und ich muss dann also in
jedem Falle die alte Variablenbindung wegnehmen, die neue Variablenbindung eintragen.
Und ich muss Oh, das Semikoleon ist leider Gottes hier
hinten etwas an den Rand gedrängt worden.
Alternativ dazu, wenn das Ding nicht existiert,
muss ich sagen, das ist eine Unbekannte Variable.
Deren Wert kannst du nicht setzen. Das ist der einzige
semantische Unterschied zwischen Define und Set Bang. So,
und damit haben wir die elementaren Mechanismen erstmal um Variablen einen Wert zuzuweisen.
Und wir können jetzt tatsächlich in Umgebungen Variablen einführen und ihren Wert wieder abfragen. Also,
wenn ich jetzt ein Atom habe und Atom sind ja unsere Namen,
dann ist es eine Variable und ich kann den Wert dieser Variablen abfragen,
aber immer bezüglich einer bestimmten Umgebung, in der das passiert.
Ich kenne jetzt also in der globalen Umgebung,
setze ich X auf den Wert 5,
ich gebe hier immer Trut zurück, das ist,
wie gesagt, undefiniert, welchen Wert das haben soll.
Und ich kann jetzt in dieser Umgebung der globalen Umgebung
den Wert von X abfragen und ich kriege tatsächlich den Wert,
den ich vorher da reingesetzt habe.
Also jetzt haben wir diese Funktionalität für Variablen da,
wir können Variablen einen Wert geben, wir können den Wert wieder abfragen.
Und das Ganze kann in bestimmten Umgebungen passieren.
All das haben wir jetzt im Kasten
Gut, das nächste ist die Funktionsanwendung.
Das hatte ich ja schon gesagt. Funktionsanwendung heißt immer,
ich habe einen S-Ausdruck, eine Liste mit N-Elementen.
Das erste Element ist der Funktionsname. Die restlichen sind
die Argumente der Funktionen. Und die funktionale Applikation besteht immer
aus dem Evil-Schritt und dem Apply-Schritt.
Das Auswerten nimmt das erste Element her. Das muss zu
einem Lamde-Ausdruck auswerten und die restlichen Elemente werden die aktuellen Parameter,
indem ich
ihren Wert ermittle
Und im Apply nehme ich dann den Lamder-Ausdruck vom ersten Element
und wende ihn auf die Werte der restlichen Listenelemente an.
Ausnahme davon sind die sogenannten Special-Form-Expressions, die eben Abweichungen von dieser Standard-Auswertungsreihenfolge definieren.
Die müssen in jedem Einzelfall gesondert implementiert werden, genau wie
wir das jetzt eben für Set Bang und The Fine gemacht haben.
Müssen wir das für alle anderen Special-Form-Expressions ebenfalls tun?
Und der Grundsatz, es gibt einen Grundsatz für Special-Form-Expressions,
dass der Funktor nie ausgewertet wird. Also das,
was wir hier bei der Funktion haben,
dass wir das erste Element immer als zu einem Lamdeausdruck auswerten können,
das geht für die Special-Form-Expressions nicht,
weil eben deren Semantik fest verdrahtet ist und nicht
per Defien zugewiesen werden kann
So, da haben wir verschiedene Fälle.
Der einfachste Fall ist das Quote, Quote heißt Wende,
Werte, das Argument von Quote nicht aus.
Unterdrücke die Auswertung. Das brauche ich beispielsweise, wenn ich
über einen Atom reden will und dies nicht eine Variable sein soll.
Wenn ich also beispielsweise hier das Atomhaus habe und ich möchte
wirklich Haus als Literal haben und nicht die Variable namens Haus,
dann kann ich das quotieren.
So sieht das in Scream aus, beziehungsweise dieses Quote haben
wir nicht erlaubt. Wir müssen also dann in diesem Falle
Quote Haus schreiben und meinen damit wirklich
das Literalhaus
Das Gleiche gilt für Funktionen. Wenn ich also so etwas habe,
fuh von 2 und 3 und ich gebe das so in das System ein,
würde er sofort diese Funktion auf diese Argumente anwenden.
Wenn ich das nicht will,
wenn ich also beispielsweise über die Liste A, B,
C reden will, dann muss ich das quotieren.
Dann wird es vor Auswertung geschützt und jetzt rede ich über die Liste A,
B, C und nicht über den Funktionsaufruf,
der den Wert der Funktion A bei Anwendung auf die Werte B und C berechnet.
Das ist die Verwendung oder die Bedeutung,
weshalb wir das Quote überhaupt brauchen
Das heißt, einzig funktional, einzig verwendungsweise ist,
ist die Auswertung eines Ausdruckes zu verhindern.
Entweder für Namen oder aber für Listen. Gut, das ist
der einfachste Fall. Die Nächsten sind komplizierter. Fall 2
ist, Argumente werden teilweise ausgewertet, dass sie feiern,
hatten wir schon. Der Name darf nicht ausgewertet werden,
dieser Ausdruck hier hinten sehr wohl,
weshalb wir also so etwas machen können,
wie die fein X zu Plus 3,
2. Heißt,
setze den Wert X
auf diese Summe
Dann hat X den Wert 5 eben. Hier wird ausgewertet
, hier wird nicht ausgewertet. Wenn hier ausgewertet würde,
würde ich eben den Wert von X,
der möglicherweise undefiniert ist, auf irgendetwas setzen wollen.
Und das ist nicht gemeint. Das ist die vajable X
tatsächlich selbst. Das Gleiche gilt also für das Set Bang.
Und auch dort wird der vajablen Name vor Auswertung geschützt,
die Auswertung hier hinten erfolgt tatsächlich.
Also nur Expression wird ausgewertet. Fall 3 ist eine
bedingte Auswertung, das brauche ich für die Konditionale.
Im Konditional muss ich natürlich die Bedingung immer auswerten.
Ich muss ja wissen, welchen Wahrheitswert die hat
Diese Bedingung hier war oder falsch ist.
Das muss immer ausgewertet werden, aber hier hinten wird natürlich
nur in Abhängigkeit von dem Ergebnis hier vorn ausgewertet.
Wenn das hier zu True auswertet, dann wird dies ausgewertet,
aber das nicht. Und wenn dies zu False auswertet,
wird das nicht ausgewertet, aber jenes. Das ist die
übliche Bedeutung des iFNLs. Und wir sehen hier das,
was wir eben in der Logikprogrammierung mit dem Cut erreichen mussten,
weil es eben nicht so richtig schön in die saubere Welt der Logik hineinpasst.
Wir müssten den Suchraum manipulieren, um es überhaupt machen zu können.
Das spiegelt sich hier wieder,
dass wir dafür eben eine Special Form Expression brauchen
Das ist die Konsequenz davon. Gut,
das Gleiche gilt also für so etwas wie End- und Ohr auch,
oder beide brechen immer dann in dem Augenblick ab,
wenn der Funktionswert feststeht,
also wenn das End das erste Mal ein False auswertet hier,
dann bricht die Verarbeitung ab, dann werden die restlichen überhaupt nicht mehr angefasst.
Genauso umgekehrt, wann immer das Ohr zum ersten Mal einen Truh auswertet,
dann kann es abbrechen, denn er weiß den Funktionswert bereits.
Das kann ich dazu ausnutzen, dass sich das
End- oder Ohr als verkapptes If
auch verwende
Ja, dass ich eben eine... Ja,
kein If and Else, sondern nur ein If,
wenn, kann ich dadurch realisieren, dass ich sage,
ich habe hier eine Bedingung und wenn die wahr ist,
muss das auch noch ausgewertet werden.
Wenn die bereits falsch ist, dann komme ich hier nie dazu.
Das ist so ein paar Cup des If-Fan.
Letzte Variante ist, dass der Ausdruck transformiert wird,
dass es meines Wissens passiert, dass beim Kont,
dass der eben intern umgewandelt wird in einen geschaffelten Ift-Ausdruck
Kont hat ja, oder sorry, das muss ich dazu sagen,
ein Kont ist also eine Selektion aus verschiedenen Alternativen.
Ich kann also beispielsweise sagen, Kont,
wenn ein X kleiner als zwei ist,
wenn es kleiner als fünf ist,
wenn es kleiner als sechs ist und so weiter und er nimmt dann eben jeweils die Alternative,
die als erstes passt, das wird in IF-Ausdrücke aufgelöst.
Gut, damit haben wir die wesentlichen Fälle zusammen.
Die Argumente werden nicht ausgewertet, das ist natürlich extrem einfach,
denn das ist wieder umgebungsunabhängig, das muss in jeder Umgebung genauso passieren.
Wenn ich einen Ausdruck des Typs Quote irgendetwas habe,
dann ist der Wert dieses irgendetwas hier drin.
Das ist genau das, was wir hier hatten.
Also wo haben wir es hier? Ich habe ein Quotehaus,
dann ist der Wert eben genau das, was hier steht.
Oder ich habe hier ein Quote, diese Liste,
dann ist es eben nicht der Wert dieses Funktionsaufrufes,
sondern die Liste selber, über die ich rede.
Gut, entsprechend einfach ist die Definition. Und wenn ich das teste,
ich gebe jetzt einen Quote X-Ausdruck ein,
dann kriege ich eben als Wert tatsächlich den Vagablen oder den Atomnamen und nicht den Wert,
den ich irgendwann mal an eine Vagable X gebunden habe
Zweites Beispiel war die, die Argumente werden nur teilweise ausgewertet.
Da greifen wir jetzt zurück auf das, was wir eben
definiert haben, nämlich diese Prädikat, die Fine Variable.
Das benutzen wir hier für die Realisierung der Semantik.
Alles, was wir jetzt noch klären müssen, ist,
was für welches Pattern das Ganze verwendet wird und was mit den Einzelelementen passieren muss.
Also, ein Defying hat immer zwei, es gibt zwei Möglichkeiten,
das ist das Erste, das war die Kurznotation,
das ist die normale Definition mit einem Landerausdruck.
Hier gebe ich Das ist das Schlüsselwort, die fein,
das ist die Liste der Variablen, die ich in der Funktion habe.
Und das ist der Körper der Funktion. Ich definiere eine Funktion wie jede andere Definition,
passiert in einer bestimmten Umgebung und sie muss einen Wert liefern
und als Wert der Wert ist wieder undefiniert,
ich habe hier einfach okay reingeschrieben.
Heißt, habe ich zur Kenntnis genommen. Ich muss natürlich sicherstellen,
dass das Atom hier, dass diese Variablen,
einen Variablen Name ist, das rede ich über das Atom,
dann weiß ich, das ist ein Einfach nur meerische Konstante.
Ich werte den Ausdruck hier hinten aus, ganz einfach,
indem ich es in der gleichen Umgebung,
wo dieses hier ausgewertet wird, auch in die Auswertung schicke.
Dann habe ich den Verb dieses Ausdruckes und dieser Wert des Ausdruckes wird jetzt hier,
in das Define Variable hineingesteckt und das trägt mir das Ganze
in der entsprechenden Stelle in die Datenbank ein.
Auf die Art und Weise wird jetzt hier also
eine Funktionsdefinition in Empfang genommen und in die Datenbank eingetragen
Nichts anderes passiert da. Unter der Bedingung,
dass das Ganze tatsächlich an ein Atom gebunden wird.
Denn nur dann macht es tatsächlich Sinn nicht.
Das muss ein Vajablenname sein. Das ist die einzige Bedingung.
Ansonsten können hier hinten eben jetzt Zahlen auftauchen, wie die feinen X zu 2.
Es können Funktionsaufrufe auftauchen, das ist alles egal.
Es können eben auch Funktionsdefinitionen sein. Das wird alles durch
dieses Defeien erledigt. Für Funktionsdefinitionen hatten wir noch die Kurzfassung zur Verfügung,
wo ich jetzt also hier nicht eine Vajable angebe,
sondern des Aufrufpattern bestehend aus dem Funktionsnamen und seinen Parametern,
die ich der Funktion übergeben möchte und dann muss ich eben das in Form einer Closure dort eintragen.
Was eine Closure ist, kriegen wir später.
Das ist eine spezielle Notation für Funktionen,
intern, die besteht aus der Funktionsdefinitionen und der Umgebung,
in der die Funktion definiert wurde.
Warum wir das brauchen, das kriegen wir später.
Das ist die Closure, hat jetzt tatsächlich hier die Parameter der Funktion,
die Funktionsdefinition selber und die Umgebung, in der sie definiert wurde.
Ich kann dann also immer Rekonstruieren zum Aufrufzeitpunkt,
in welcher Umgebung diese Funktion tatsächlich gültig ist.
Und insbesondere, welche freien Variablen sie aus der Umgebung benutzt,
das gucken wir uns aber dann später an.
Ja, und jetzt sind all diese Fälle abgedeckt.
Wir können also die Variable X auf fünf Sätzen in der globalen Umgebung.
Und wenn wir sie in der globalen Umgebung wieder abfragen,
kriegen wir den Wert fünf. Wir können Y als Funktion definieren,
die 1 und 2 addiert in der globalen Umgebung.
Und wenn wir Y in der globalen Umgebung aufrufen,
dann kriegen wir eben den Wert drei.
Der durch die Berechnung von 1 dieser Addition hier zustande
gekommen ist
Wir definieren hier eine Funktion. Und wenn wir
den Wert dieser Funkt eher dieser Viablen Square anschließend abfragen,
kriegen wir die Closure genau, die wir hier eingetragen haben.
Ich bin eine Funktion mit einem Parameter. Ich multipliziere meinen
Parameter mit sich selbst, um den Funktionswert zu ermitteln.
Und ich bin in der globalen Umgebung definiert worden.
Das ist das, was hier steht.
Gut, dass die Feinden haben wir abgearbeitet. Oh,
keiner Mord,
das ist schlecht
Alle schon eingeschlafen. Gut, also, dass die fein
haben wir schon definiert. Das Set Bang haben wir
noch nicht definiert, das müssen wir demnächst machen.
Ich werde also Gelegenheit haben beim Define nochmal aufzusetzen.
Und das nächsten Freitag, bis dahin wünsche ich Ihnen
ein schönes Wochenende