Ich hatte ja schon häufiger versprochen, dass wir über Meta-Programmierung sprechen wollen.
Also wie erweitern wir unser Racket, um andere Programmierstile benutzen
zu können. Und genau darum soll es heute gehen.
Wir haben ja schon zwei eingebettete Programmierstile verwendet. Also Racket
kommt von sich aus mit der funktionalen Programmierung sehr gut vorbereitet.
Die haben wir in zwei verschiedenen Sprachvarianten kennengelernt.
Einmal mit verzögerter Auswertung und einmal mit vorgezogener Auswertung.
Die verzögerte Variante ist die Variante Lesi
Gehalten. Diese beiden Programmierstile sind durch Einführen von neuen Sprachelementen
direkt in Reckett mit die feinen Sonntags eingeführt worden.
Wir haben in der Vorlesung noch zwei weitere Verwendet.
Das eine am objektorientierte Programmierung, in dem das kommendes Objectsystem Zählers
eingebaut worden ist. Dafür, um das möglich zu machen,
mussten auch ein paar neue Sprachelemente eingeführt werden,
wie die feinen Glass oder die feinen Method.
Aber im Wesentlichen reicht es, die Syntax zu erweitern,
mit wohlüberlegten Sprachelementen
Ja, dann haben wir ProLog in Reckett eingebaut.
Lange nachdem ich das gemacht habe,
ist inzwischen auch ein eigenes ProLog-System ein Reckett eingebaut worden,
genauso wie es ein eigenes Klassensystem gibt für die Objektorientierte, Programmierung.
Sie können sich dafür auch direkte Sources anschauen, wenn Sie Ihr Reckettpaket aufmachen,
sehen Sie, wie all diese Erweiterungen implementiert sind.
Worüber ich heute sprechen möchte, ist,
wie man das eigentlich macht, sich Sprachelemente überlegen,
die in die Sprache einfügen und dann mit einmal
in meinem anderen Programmierstil programmieren
zu können
Ich werde es nochmal ans Beispiel zeigen, wie bauen wir
die Stromorientierte Programmierung ein? Das ist das, was in
der Reckage-Sprache Lesi eingebaut ist, ja. Und überhaupt,
wie macht man die Metaprogrammierung mithilfe von Makros oder die Fallen
sind, da schauen wir uns gleich an.
Und zum Schluss auch noch, was passiert,
wenn wir den reinen funktionalen Programmierstil verlassen, Sprachelemente nehmen,
die dich bezugstransparent sind und für welche Teile lohnt sich das,
also programmieren mit Zuständen,
Programm für heute
Vorschau aufs nächste Mal, gleich das nächste Mal,
möchte ich Ihnen noch eines der größeren Beispielpakete zeigen,
die ich für diese Vorlesung programmiert habe, nämlich den General Problem Salver.
Der passt sehr gut als Ergänzung zu dem,
was wir über Relationales Programmieren gelernt haben und ist hoffentlich auch neu für diejenigen,
die relationales Programmieren aus dem FF schon,
aus Volkgang Menzels Vorlesungen kennen und zum Zweiten werde ich das nächste Mal die zweite Hälfte der Vorlesung nehmen,
um einfach nochmal durch Semester durchzugehen und zu betonen,
was mir wichtig war und was ich erwarte,
dass sie es in der Abschlussklausur können.
Okay, Zur Meta-Programmierung hatte ich ja,
hatte ich schon erwähnt, gibt es eigentlich in Recket.
Zwei Wege. Der offizielle Weg und gut unterstützte und der,
die man rein gewiss folgen kann, ist,
dass man das in Recket eingebaute, die feinen Syntax nimmt.
Das ist so vorbereitet, dass bestimmte Probleme verwenden werden,
die sich ergeben, wenn man die Quick & Dirty Variante nimmt, Makros.
Ich habe lange mit mir gekämpft, wie erzähle ich
es hier in der Vorlesung? Ich habe gedacht,
für sie wird es viel nützlicher sein, erstens zu wissen,
dass es die reine saubere Art gibt und zweitens zu wissen,
warum Makros Schwierigkeiten haben. Weshalb, weil die meisten Programmiersprachen,
die man für effizientes Programmieren, Echtzeitprobleme und so benutzt,
weil die im Wesentlichen nur die Makros bieten.
Also sollte man als Informatikerinnen und Informatiker wissen,
auf was man sich da einlässt und wo die Schwierigkeiten und Gefahren sind.
Deshalb zeige ich ihnen, wie es mit Makros geht und vor allem auch,
weil ich das in den Paketen, die ich nach Racket portiert habe,
mit Makros gemacht habe, in den ursprünglichen Quellen war es schon mit Makros drin.
Und in dem Fall sind die Makros sicher.
Also ich habe sie abgesichert, sodass man das mit Makros
trotzdem gut ging
Also deshalb erstens Welt, wie benutzen wir Makros,
damit sie wissen, wenn sie Makros in C,
C plus plus verwenden,
ja, oder auch in der IVC-Vorlesung mit Makros arbeiten,
vielleicht wird die man im nächsten Jahr auch IVC hören wollen.
Im Pafre haben wir für die Strukturierung von Programmen nichts anderes als Makros,
also sollte man schon mal wissen, was da gefährlich ist.
Und welche Fallstricke da auf allen lauern. Das sind denn Makros?
Also Makros sind zunächst mal nichts weiter als Schablonen, ja,
die ersetzen einfach die Argumente Textuell im Rumpf des Makros.
Also wenn wir ein Makro definieren, sieht es zunächst erst
mal aus, auch nach außen wie eine Funktion.
In Wahrheit aber wird ein Makro eine Auswertung machen,
bei der wir lexikanisch die Argumente unten im Rumpf einsetzen.
Überall, wo der Name des Arguments erwähnt wird.
Normalform der Auswertung. Und wenn alle Ersetzungen vorgenommen wurden,
dann wird der Ausdruck evaluiert.
Und was wir da haben mit einem Makro ist im Wesentlichen
die äußere Reduktion
Aber man fragt sich, warum das gefährlich sein kann.
Kommen wir später hin. Aber wir können diese Makros eben verwenden,
um neue Spracheelemente einzuführen. Wir haben das schon festgestellt,
als wir diskutiert hatten, was sind Special Form Expressions gegenüber Funktionen.
Mit Makros können wir das machen, was dazu führt,
dass wir neue Special Form Expressions in Matten recket haben.
Also zum Beispiel unser eigenes If einführen. Weshalb Makros sehr oft in Sprachen BC,
Semblersprachen und so verwendet werden, ist einfach Rechenzeitersparnis.
Wenn wir konstante Ausdrücke haben, Und definieren Sie als Makros
, dann können wir schon zur Übersetzungszeit ausgeführt werden.
Und unser Kot wird zur Laufzeit schneller sein.
Weil eben Makros praktisch nichts weiter tun als neue Kotblöcke einführen,
die textuell dann im Programm erscheinen.
Haben wir damit Ausdrücke, die zur Übersetzungszeit schon ausgewertet werden können?
Das Programm wird schneller. Also zum einen das Programm wird lesbarer dadurch,
dass wir Makros schreiben. Und wenn wir uns entscheiden,
statt einer Funktion ein Makro zu nehmen,
wird es einfach Inline-Code im fertigen Programm und wird zur Übersetzungszeit
übersetzt und wir haben keine Funktionsaktivierung
Okay. Jetzt nochmal fürs, für die Gewissenserforschung, ja.
Makros sind wirklich Spracherweiterungen. Also wenn immer wir ein Makro schreiben,
dann haben wir ein neues Sprachelement, eine neue Special Form eingeführt.
Und das sollte man wirklich nur wohl überlegt tun.
Also wenn wir die Versuchung haben,
ein Makro zu schreiben und damit unser Programm zu erweitern,
um neue Auswertungsstrategien, eine neue Special Form da reinsetzen.
Dann erstmal überlegen,
brauchen wir es wirklich
Dann muss die Syntags sorgfältig entworfen werden und zwar so,
dass sie zum Stil der Sprache passt, die wir schon haben.
Also nicht irgendwas völlig Neues da reintun. Dann wird,
wenn uns das gelungen ist, dann überlegen wir,
aha, so soll es aussehen.
Zum Beispiel in der Robotiksprache könnten wir schreiben,
Bewegeblock A zu Block B oder auf Block B, ja.
In den Macro Rumpf schreiben wir rein, wozu das expandiert werden soll.
Und dann wird das Ganze mit Defmakro definiert. Okay,
von Peter Norwick, die Warnung, First Step and writing a mecro.
Is to recognize that every time you right one?
You are defining a new language? That's just like lispe
. Er hat es damals für Lisp gesagt.
Except for your new mcro. The programmer with things that
way. Öbi. Boah, das ist gefährlich.
Wilbi. Reitvolle, extrem defrugal in defining mcross.
Bis sides. In some my nast, what did you
do today? Sound smore exempressive to say,
I defined in new language and rule the compiler for it.
Then to say, I just take that a couple of mcross.
Also das ist eigentlich das, was wir tun,
wenn wir Makros einführen, eine Sprache besser benutzbar zu machen.
Wir sollten es tun mit der Idee, dass wir eine
neue Schnittstelle bieten, durch die es benutzt werden kann.
Und zum Beispiel eben neue Sprache, neuen Programmier still einführen, ja?
Okay. Introducing a macrop it's much more memory strain on
the reader of your program, then there's introducing a function,
variable or data type.
So it should not be taken lightly. Introduce macrops only and
there is a clear need, and then the macrophits and varow
was your existing system. As see, are how put
it. On think the language Designer should not do is
to include untright ideas of its own.
Also wenn wir neue Programmiersprachen entwerfen, nicht einfach nur,
weil es uns Spaß macht, da ein paar Sprachelemente reinführen,
die einfach nicht zu den bestehenden Elementen passen.
Dass man einen solchen Wildwuchs bekommt,
dass das Ganze hinterher nicht mehr leicht zu verstehen ist,
weil es einfach zu viele Varianten der Selbstsache gibt.
Okay. Nehmen wir mal ein Beispiel. Def Makro,
was wir verwenden wollen, kommt aus der Sprache des Windel.
Und Wir verwenden jetzt Zwindel, um, dass unser altes
Problem mit Elissa P. Hacker und ihrem My-If zu lösen.
Eine unserer ersten Übungsaufgaben, ja, war ja,
was ist der Unterschied, wenn ich eine Funktion My-If schreibe gegenüber dem Üff,
was schon in der Sprache drin ist.
Wir hatten gesehen, wir brauchen eine Special-Form Expression, um zu erreichen,
dass die Auswertereihenfolge der Argumente MIf in einer bestimmten Reihenfolge erscheint.
Erst wenn wir wissen, ob der Erzzweig ausgeführt werden soll,
soll das Argument ausgewertet werden.
Und das können wir eben mit der Makro machen.
Wir definieren hier ein Makro, das sagt,
dass unser neues Sprachelement My If Wann enthält hier die Bedingung, ja.
Und dann den Wendteil und den Sonstteil. So. Und es soll
expandiert werden zu einem bestimmten Recket-Code und dieser Recket-Code beginnt mit dem Wort Cont.
Und dann kommt eine, wie sieht ein Cont normalerweise aus,
kommt, dann kommen ja die Paare.
Was soll geschehen, wenn die Bedingung erfüllt ist?
Ja. Erste Bedingung, dann soll das geschehen.
Zweite Bedingung, wenn das war, es soll das
geschehen und so weiter
Also hier bauen wir jetzt einfach diese Paare auf,
Bedingung, wenn. Also hier wird die Liste konstruiert,
da kommt die Bedingung rein und das wenn.
Und das folgt dem Wort kommt. Und dann kommt das,
was im Erzzweig geschehen soll, beginnt mit dem Symbol Erz
und dann, was im Sonstteil steht, ja.
Und das Ganze ist eine Liste. Also das Makro baut
diese Liste zusammen. Und das ist der Code,
der eingesetzt wird in unserem Programm. Wenn jemand schreibt,
my if und dann hier Bedingung wendern, ja,
dann wird die hier so zusammengebaute Liste genommen und da reingesetzt
Ja, also ehrlich gesagt, die Grundideen,
Makrosäulenprogramm lesbar machen und diese lange Kaskade von Lists hier,
selbst für dieses Trivialbeispiel, das ist vielleicht schon ein bisschen abschreckend.
Es gibt eine einfachere Art, das zu schreiben und das ist das Quasi-Quot.
Wir haben verschiedene Arten von Fotierung und wir können diese Art
die Liste zusammenbauen mit Quasi-Quot, Quote einfach so schreiben.
Hier ist also das Quot des Rückwärtsquot. Und wenn das geschieht,
dann das irgendwo verwendet wird,
dann Wird Dreck jetzt hingehen und überall da,
wo hinter dem Quot ein Kummer kommt, Teile auswerten,
in dem Moment, wo diese Liste genommen wird.
Also dieses Komma ist ein Unquotieren,
ja, also wenn ich hier das quasi Quot mache,
dann wird Klammer auf Kont gelesen als plodierte Liste.
Auch diese Klammer hier noch, aber hier drin kommt jetzt
das Komma. Und das sagt, werte das aus,
was hier steht und das führt dazu,
dass die Bedingung, die hier als Argument übergeben wurde,
hier reingesetzt wird als Bedingung und nicht als quotiertes Wortbett
Genauso für den Wendteil und für den Sonstteil. Die anderen
Teile ohne Komma davor werden wir wörtlich genommen, ja?
Aber die Argumente werden eingesetzt. Und das ist die klassische Art,
wie man in allem Liz-ähnlichen Sprachen Lyfts geben, Recket,
Makros schreibt, man nimmt das quasi Quot, um einfach Listen zusammenzubauen,
die quotiert sind und wo Teile eingesetzt werden, je einfach das Komma davor.
Ja, und hier kommen wir jetzt, sehen wir jetzt auch genau,
was geschieht, wenn wir unser Makro, MyF2 schreiben.
Wir haben das konditional, wenn die Bedingungen erfüllt ist,
wird der Wendteil ausgeführt im Erzfall,
sonst das,
Und genauso können wir diese anderen bedingten Teile schreiben,
wie das Wenn oder das Anläss,
was ja schon fertig in Recke drin ist, ja.
Ein Wenn, das ist ein einseitiges Üff,
wo es eben für den Erzzweig das Angefallen zurückkommt.
Und das könnten wir so schreiben. Üff Test,
ja, hier wird jetzt unsere Bedingung hier wieder eingesetzt,
weil hier haben wir das Quasiquot.
Und hier das Komma, um das hier in Anquot
zu machen.
Und jetzt wollen wir
Dass alles, was hier nach dem Test an Anwendung,
an Ausdrücken kommt, also da kann eine endlos lange Liste von Ausdrücken stehen.
Und die werden übergeben und zusammengefasst als eine Liste,
die Body heißt. Was wir jetzt machen wollen,
ist, dass hier einfach dahinter flach all diese Ausdrücke stehen.
Und das macht dieses Amtsbleis, dieser Klammeraffe hier,
der sorgt dafür, wenn wir eine Liste übergeben,
und Rest Body
Tierungsanquote davor machen, dass hier einfach flach die Liste dahinter steht.
Also so ein Trick ist auch gut, wenn man, sagen wir,
so was wie ein End machen will oder einfach etwas zusammenbauen,
wo das, was man als Liste bekommt, flach dahintersteht.
Man will nicht unbedingt eine Apply haben, sondern möchte es wörtlich einsetzen.
Und das macht eben Disanspleis hier, ja.
Mit dem Anquote davor kriegen wir jetzt einfach alles,
was hier im Rest als Argumente,
als Liste zusammengefasst worden ist, wieder einzeln hier oft gezählt,
mit dem Beginn davor wird es zu einer Sequenz.
Ja, und wenn es nicht erfüllt ist, geben wir vor als zurück.
Ich hätte da auch eins der an die Feind-Argumente zurückgeben können.
Ja, nutzen wir das mal, um jetzt imperative Sprachelemente einzuführen.
Zum Beispiel die Waldschleife. Die ist in Racket schon fertig
. Wir haben sie auch ins Windel,
wenn wir einfach eine Schleife schreiben wollen, solange ein Testausdruck wahr ist,
sollen all diese Ausdrücke der Reihe nach ausgeführt werden.
Ja, und dann immer wieder zurücktesten ist Test jetzt
wahr oder nicht mehr wahr. Können wir so schreiben,
das Ding mit einem Led Dreck.
Wir haben hier eine Funktion namens Loop
Dieses Funktion namens Loop wird hier eingeführt als Lamnder.
Und da ist unser Ben-Test drin. Also wenn der Test
erfüllt ist, dann führe bitte all diese Expressions hier aus.
Und andernfalls rufe dich selbst wieder auf. Loop wird hier
einfach ohne Argumente selbstrekursiv aufgerufen. Und dieses Laptrek,
das wird hier eben unten Club aufrufen. Warum brauchen wir hier ein Lett,
Lettschreck und nicht ein Lettschter und nicht das simple Nett,
einfach weil Die Funktion Loop, die hier definiert wird, sich selbst rekursiv aufruft.
Und das geht eben nur, wenn wir den Dreck verwenden.
Da hilft selbst Letztsternen nicht. Das ist mal ein Beispiel,
wo man sieht, dass man das Lab-Track braucht,
wenn wir eine lokale Funktion rekursiv definieren wollen, durch sich selbst.
Okay, und wenn wir das mit einem Makro machen wollen,
also unsere eigene Waldschleife definieren, also all diese Expressions,
die hier hinter stehen, wir müssen ja nicht viel,
viel Expressions da kommen sollen,
bleibt also nur dieser Trick mit dem Untrestargument und hier
dem Body im Body
Die Variable Body wird wieder die Liste aller Expressions 1 bis N sein.
Ja. Jedes. Quasi Quote am Anfang wieder.
Lad Track, Loop Lambda, das ist alles un,
ja, viel fest quotiert.
Wir müssen nur wieder, das Komma hier vorm Test machen,
weil da der echte Test als Argument.
Eingesetzt werden soll. Und der Body hier muss wieder
mit Ansplice rausgenommen werden. Entschuldigung. Und dann
YouLube aufgerufen.
Also ins Findel ist fertig, weil ein Till ein Do-Time-Stuhlist.
Besonders schick finde ich das hier. No Arrows. Solange
wie kein Laufzeitfehler auftritt. Für den Body auf,
ja. Eine gute Idee. Dankeschön. Was leider meinen
grippalen Effekt nicht los, die Semester.
Mal gucken,
ob der hilft
Wirkt schon gut, aber ich kann jetzt nicht mehr sprechen.
Okay. Also all diese Funktionen sind fertig und wir können
also mit Hilfe von denen unser schönes Recket,
wie eine imperative Programmiersprache verwenden, ja,
mit den Funktionen können wir Waldschleifen schreiben,
Vorschleifen ist es eine wahnsinnige Du-Vorschleife in Zwindel drin,
also die eignet sich eigentlich für jede Art von Schleifen,
die man je braucht,
Und mit diesem hier können wir jetzt zum Beispiel einfach schreiben,
weil das, solange mein Ihr kleiner als Szenen ist,
in Beng, ja, das Imperative erhöhe I um 1
und dann schreibt es aus und wir kriegen 8, 19.
Also wir sind jetzt praktisch in der Welt der Imperativen Programmierung angekommen,
können wir mit Hilfe von Makros eben in den Programmierstil einbetten, ne?
So, effizient, die wir erreichen können, Makros,
können wir die Berechnung von Ausdrücken
schon zur Übersetzungszeit ausgegeführt werden
Jetzt wusste ich noch was. Suchen wir es noch mal
neu? Aha, weiter darf mich nicht rantrauen.
Okay, also Zum Beispiel, wo es sich lohnt,
wenn wir eine lange Liste haben. Wir wissen ja,
die Funktion längst, der bleibt nicht viel anderes übrig,
als die Liste durchzugehen und zu zählen.
Wie lange ist sie denn und je länger die Liste,
desto länger dauert das.
Oder Multiplikation. Das können wir mal an einem Beispiel testen.
Ich teste dich einmal
Mit einer Funktion Everage. Und dass wir die Länge für
die Argumente bestimmen und dann sie alle aufsummieren, ja.
Wenn wir es aufrufen, kriegen wir ein Ergebnis.
So. Und das Ganze. Auch mal als Funktion hier oben.
Everage F. Wo wir einfach ein Applied Plus für die Argumente machen.
Das ist nicht das Mikro, das sind Huhrarbeiten außerhalb,
ne? Also hier muss irgendwie eine Zahnarztvorlesung nebenan sein.
Na gut
Jetzt nehmen wir die Funktionen time. Time ist einfach die Funktion,
die einen Ostdruck ausführt und die Stoppuhr mitlaufen lässt,
wie lange gedauert das? Ja, wenn wir jetzt hier
eine Million Mal Everage als Makro aufrufen, sehen wir,
dass die CPU-Zeit halbe Sekunde ist und auch die Real-Time auch und überhaupt keine Gabels-Collection.
Wenn wir es als Funktionen machen, hier eine Million Mal,
wir sind der Ausdruck ausrechen entlassen,
dann sind wir schon bei knapp drei Sekunden in CPU-Zeiten und Riotscheiben auch und ziemlich viel Gobbage-Collection.
Durch den Aufruf als Funktion, weil eben immer ein Stack aufgebaut wird.
Ja. Das Makro spart also nicht nur den Aufwand für
die Rechenzeit, sondern auch für die Gobbage-Collection.
Sehr schön. Ah, jetzt klingt es auch noch zu.
Die Nase zu laufen, hat auch jemand Nasenspricht,
dann tust du morgens. Ja, gut, also Ich hoffe,
das war erstmal ein gutes Verkaufsargument des Makros-ärztins- und Programm übersichtlicher machen,
dass wir uns erlauben, andere Sprachelemente aus meinem Programmierstil zu verwenden.
Und jetzt möchte ich noch ein bisschen drauf eingehen,
worauf man achten muss, damit man nicht genau das Gegenteil erreicht.
Also es hat so seine Schattenseiten, keine Hosen ohne Dorn.
Und das Schlimmste, was einen treffen kann, ist die
unbeachtsichtigte Mehrfachauswertung. Erinnern wir uns, ganz zu Anfang,
als ich gezeigt habe,
was die Makroexpansion macht
Und die macht genau das, was die verzögerte Auswertung tut.
Den Code vom Rumpf expandieren und überall,
wo die Variable kommt, dann die Argumente unausgewertet reingesteckt.
Und wenn wir da zehn Stellen haben, wo wir die
Argumente unausgewertet reinstecken, müssen wir das Ganze hinterher zehnmal auswerten.
Also das kann dauern. Und? Das Zweite ist,
die unausgewerteten Argumente, die landen im Code irgendwo innen drin.
Und die können da dann, wenn der Code expandiert wird,
in einer völlig anderen Bindungsumgebung ausgewertet werden und mit einmal
zu anderen Werten evaluieren
Ein kleines Beispiel für den Teil. Also bei Makros besteht
die Gefahr nicht nur, dass das Ganze länger dauert,
wenn wir es falsch machen,
sondern dass wir auch ein völlig anderes Programm schreiben.
Was andere Dinge tut, als wir gedacht haben.
Also wenn wir jetzt mal x hoch 8 rechnen wollen,
also 8 der Patenz von x berechnen.
Können wir das so schreiben? Der Makropower, vorne das Quasi-Quot-Zeichen.
Und dann schreiben wir unser x8 mal rein und im Komma davor.
Und überall da, wo das x mit dem Komma steht,
wird das x aus dem Argument genommen und reingesetzt
So, wenn wir jetzt Power 8 von Sinus 1 berechnen,
ja, dann haben wir einen ziemlich hohen Aufwand, eine Mölche,
fünfte Sekunde um, ne, ist eine Viertelsekunde, ne?
Weil Sinus 1 in diesem Beispiel achtmal ausgerechnet werden und wir wissen,
dass es die trygonometrischen Funktionen wirklich rechenaufwendig sind.
Wir haben den Ausdruck nur einmal geschrieben. Bei vorgezogener Auswertung wäre es
nur einmal ausgewertet worden, wenn es eine normale Funktion wäre.
Wenn wir das als Funktion schreiben und jetzt wieder unseren Test
mit Time machen, eine Million mal diesen Wert ausrechnen,
Als Funktion haben wir dreieinhalb Sekunden, ja.
Aber wenn wir es als Makro machen, ah Quatsch,
falsches Behalte. Also hier mit den Makro-Varianten, dreieinhalb Sekunden,
die Variante als Funktion, nur 1,7, also ungefähr die Hälfte gebraucht.
Aber die Seiteneffekte sind eigentlich das, was wirklich tückisch ist.
Ich meine, mit einem Faktor 2 kann man leben, ja.
Das ist, wenn es übersichtlicher wird, kann man es
schon mal in Kauf nehmen, außer es ist eine Echtzeitanwendung.
Aber jetzt nehmen wir mal ein Beispiel, wo wir zusätzlich Seiteneffekte im Makro haben.
Zum Beispiel dieses Ink F X in GF ist einfach die Funktion,
die eine Variablen nimmt, ihr ein,
sie um eins erhöht und also den Wert einer Variablen wirklich ändert.
Könnten wir so schreiben, Ink Bangverbänden X und das Resultat
ist die Variable X, die zurückkommt.
Wenn wir jetzt hier ein X als eins definieren,
nochmal zur Schreibkonvention, wenn immer wir Sterne links und rechts
um die Variablen haben, das war ja unsere Konvention,
dass das globale Variable sind
Okay, also und jetzt rufen wir in etwa nix
auf und dann kommen zwei zurück,
weil hier war es schon mal als eins definiert.
Wenn wir jetzt die vierte Potenz als Makro aufrufen,
dann hatten wir ja viermal x eingesetzt. So,
und wir wollen die vierte Potenz von x um eins erhöht haben,
dann kriegen wir oh Überraschung, 360 raus.
Warum? Weil bei der Makroexpansion jetzt für jedes X hier in KF eingesetzt wird.
Das ist das Argument, was wir übergeben haben.
Das heißt, X wird insgesamt viermal erhöht.
An jeder Position wird es
neu erhöht
Und wir bekommen hier zweimal, dreimal, viermal fünf.
Und damit natürlich einen anderen Wert,
als wenn vorgezogenes X einmal erhöht wurden, wäre eingesetzt,
ja, wie es eine Funktion tun würde.
Und wir würden das richtige Wert zurückkommen, ja.
Also das Problem ist eben, wenn wir uns hinterher,
nachdem wir unser Makro ausgeführt haben, anschauen,
welchen Wert X jetzt hat, ist inzwischen sechs.
Übergeben haben wir hier die zwei, ja. Sie ist
eben an jeder Stelle, es ist einmal erhöht worden.
Das heißt, Also eine Mehrfacherwertung, Auswertung haben,
bei der Seiteneffekte sind, werden diese Seiteneffekte so oft ausgeführt,
wie das X hier verwendet wird.
Also zum Beispiel, das Schlimmste, was man tun könnte,
wäre, random X übergeben. Ja, dann würde man
für jedes X hier eine zufällige andere Zahl angesetzt haben.
Wir können es im Trace sehen,
also wenn wir hier nochmal unser Power,
als vor vierte Potenz als Makro aufrufen und eine Funktion Show verwenden,
die einfach ein Dreitlein macht von dem Wert,
Show in F von X,
Dann bekommen wir hier bei jedem Aufruf wird, sehen wir,
dass jedes Mal, wenn auf X zugegriffen wird,
dann wird das Show in F von X gemacht und wir
sehen beim ersten Mal über das X von 2 verwendet,
dann von 3, dann von 4,
dann von 5 und das Ergebnis ist 120.
Wenn man dasselbe mit Remne machen, haben wir jetzt hier
vier verschiedene Werte für X, mit denen gerechnet wird.
Also das ist die eine ganz große Gefahr, unser X,
dass wir, wenn wir Seiteneffekte haben und die Variable im Rumpf mehrfach,
gebraucht wird, dass die Bezugstransparenz verletzt ist.
Und wir in dem Fall nicht gleiche Teilausdrücke mit gleichen Werten haben. Das Zweite,
was ich noch für viel heimtypischer halte, das ist,
dass wir eine völlig andere Auswertung des Ausdrucks bekommen können,
einfach dadurch, dass das, was wir als Argument schreiben,
ja zunächst im Bindungskontext des Aufrufs definiert ist.
Und so erwarten wir, dass es ausgewertet wird.
Wenn aber genau dieser Text irgendwo im Body eingesetzt wird,
kann er in einen anderen Bindungskontext eingesetzt werden und da haben die Variablen einen anderen Bezug.
Das ist genau der Grund, weshalb Reggit, die Sprachelement,
die fallen Syntax hat, die fallen Syntax verhindert,
dass bei der Auswertung von Argumenten sich der Bindungskontext ändert.
Deshalb heißt das in Reggit hygienischem Makros,
wenn man mit die feinen Syntax arbeitet, da kann man sich darauf verlassen,
dass die Argumente, die man übergibt, ausgewertet werden,
im Bindungskontext in dem sie ursprünglich geschrieben wurden.
Das ist die große Stärke da von mir. Gut,
also jetzt schauen wir uns hier mal an, was mit den lokalen Variablen passiert.
Wir haben hier unser Makro-End-Times, wo also dieser Body hier ausgeführt wird.
Und dazu rufen wir einfach Dood Times auf,
von Ego 1 bis N und hier kommt der Body rein.
Funktioniert wunderbar, wenn wir End-Times mit 5 sagen und sagen,
mach doch mal einen Push,
dass der Zahl 5 auf die Liste X ist und wir übergeben für X ist eine Liste,
die zu Anfang klärer ist,
dann kriegen wir eine Liste mit 5 mal der 5 drin.
So wollen wir
es haben, ja
Und jetzt definieren wir hier mal eine lokale Variable.
Oder genauer gesagt, eine globale Variable I von 10.
Und wir wollen jetzt das Endtimes zehnmal, das ist unser Wert von I,
der Wert 10, auf die Liste Xs geschoben wird.
Und wir überzeugen uns nochmal, Xs ist leer und IS10.
Wenn wir das Ganze jetzt aufrufen mit dem ersten Argument von 5,
Und wir schauen uns an, was die Variable,
die benutzt wird im Rumpf des Makros für einen Wert hat,
dann sehen wir hier 1, 3, 5.
Und weshalb geht das schief? Wir kriegen hier anstatt
von 10 nur dreimal die Werte 1, 3,
5 auf die Liste geschoben und nicht die Variable10,
die eigentlich verwendet werden sollte. Dieses I hier Ist,
wenn das Makro läuft, nicht unser globales I,
sondern es ist das lokale I aus dem Do-Times.
DC, wir haben eine lokale Variable namens I und die verschattet.
Dieses globale I, was wir eingeführt haben. Und in dem Kontext,
wenn unser Text hier Push-I-X ist, wörtlich eingesetzt wird für Body,
dann ist natürlich, bezieht sich dieses I natürlich auf dieses I,
was die lokale Laufariable hier ist.
Es ist unvorhersehbar, was passiert,
wenn wir Variablen Namen übergeben, die im Makro verwendet wurden.
Und das ist das Dilemma bei der Programmierung. Wenn wir
den Anwendern unserer Makros nicht den Code des Makros geben,
sodass sie sich überzeugen können, welche Variablen lokal verwendet werden,
dann ist das eine Glückssache, ob sie Variablen verwenden,
als globale Variable, die einen Namen im Makro verwendet werden.
Das kann man von Programmierern eigentlich nicht verlangen. Und deshalb ist die einzige Art,
sich davor zu schützen, dass wir lokale Variablen,
die wir in einem Makro verwenden wollen,
dass wir uns die nicht selbst ausdenken,
wenn wir das Makro definieren und so beliebte Variablennamen wie ich oder so verwenden,
sondern wenn das Programm expandiert wird, zum ersten Mal gelesen wird,
dann bitten wir Recket uns Variablennamen vorzuschlagen.
Genauso wie sie ein Passwortgenerator verwenden,
wenn sie ein starkes Passwort haben wollen,
was garantiert kein anderer verwendet und was nicht in irgendeiner Tabelle
drin ist,
Wir bitten, rettet, gib mir mal für die Laufvariable
einen Variablennamen, der noch nirgendwo im Programm verwendet wurde.
Also was Recket macht, ist,
es geht alle definierten Symboltabellen durch und sucht einen Namen,
der noch nie im Programm verwendet wurde.
Und den liefert uns hier die Funktion Jenson. Generiere Symbol,
denn die liefert neue ungebundene Namen. Und ich habe sie
mal testweise aufgerufen. Der erste Aufruf liefert die Variablen mit
dem tollen Namen 681 und dann noch ein bisschen langweilig.
Die nächste heißt G6882. Wenn wir jetzt Unser Programm schreiben,
dann machen wir das so, dass wir hier erst mal uns Variablen,
wir brauchen zwei lokale Variablen,
nämlich einmal für den Index und einmal,
wie oft soll es gemacht werden?
Wir rufen, führen diese Variabliche ein.
Für Index lassen wir uns ein Symbol generieren.
Für Times lassen wir uns ein neues Symbol generieren. So,
und hier in dem Makrocode, der jetzt erzeugt wird,
da setzen wir als Name der Laufvariable für den,
für wie oft es gemacht werden soll
Das Symbol ein, was wir zurückgekriegt haben als Times, ja
. Und dafür, wie oft es gemacht werden soll,
da setzen wir das Symbol ein, hier von Best Bed Times zurückgekommen ist.
Grüne Quatsch. Hier kommt der Index rein, Jan. Index
Jensem, die Variable, die wir uns ausgedacht haben.
Und wenn wir jetzt hier beginnen mit einem I gleich 1 und sagen End-Times,
jetzt mit diesem G, ja mit abgesichert in den Symbolen,
dann sehen wir hier eben, wenn wir es dreimal machen wollen,
Dann wird tatsächlich, dass sie einmal erhöht, zweimal,
dreimal, viermal.
Und wir bekommen hier genau die drei Aufrufe, die geplant waren.
Ja? Okay. Klar. Sie hatten for times,
also vor dem zweiten Teil, 1000 Komma,
aber vor dem zweiten Index nicht. Das genau,
um zu erklären, weshalb wir hier immer noch zwei,
drei, vier bekommen, statt dass derselbe Wert immer wieder reingesetzt wird.
Wir haben hier einmal die Auswertung, wo tatsächlich beim Times verhindert wird,
dass es In einer falschen Anzahl gemacht wird,
aber beim Index habe ich hier das Komma nicht drin.
Und deshalb wird eben an der Stelle trotzdem das Inkf mehrfach gemacht.
Genau die Unsymmetrie im Beispiel. Danke, dass Sie das
nochmal nachgefragt haben, hat die vergessen, eben zu erklären.
Also, ja, also sind dazu noch Fragen zu diesem Beispiel, zu dem Makros.
Also diejenigen, die im nächsten Jahr vielleicht bei mir IVC hören,
die werden dort in der Sprache Parfree-Animationen machen.
Und Parfree hat nur Makros. Also dann denken Sie bitte dran,
was wir hier gesehen haben. Dass wenn wir Makros auswerten,
dass diese Probleme da sind und dass wir uns davor schützen müssen.
Also ein simpler Weg, um zu verhindern,
dass Variable in falsche Bindungskontexte kommen, ist, dass man dafür sorgt,
dass man nicht ausdrücke auf die Argumente von Makros schreibt,
sondern im Racket-Code selbst vorher das einfach auswertet.
Ja, dass der ausgewertete Code dasteht und nur einen Wert.
Und in anderen imperativen Sprachen hilft es meistens, Dass man alles schön klammert.
Das Inrected ist jetzt mit dem Klammern kein Problem,
da können wir gar nichts anderes schreiben, als zu klammern.
Aber zum Beispiel in Poffrey, wenn wir dort Ausdrücke in Infixnotation schreiben,
dann kann, wenn wir vergessen zu klammern,
dass beim Einsetzen im Makro dazu führen,
dass durch Punktrechnung vor Strichrechnung die Zeile unseres Ostdrucks ganz anders aufgesplittet werden.
Also wenn Sagen wir mal, jetzt in der Sprache WC oder so.
Wir im Makro sowas haben? Dreimal. Iii. Mal
5. So. Und wir übergeben wir außen, die gleich 2 plus 3.
Wenn ihr jetzt für I2 plus 3 eingesetzt wird,
dann wird im Makro das zu 3 mal 2 plus
3 mal
Anstatt das hier in der Mitte geklammert eingesetzt werden würde,
2 plus 3, ja.
Hier für das I. Also wenn Sie irgendwo bei der C-Programmierung
oder Imperativen Programmierung in Sprachen mit Infixnotationen Makros schreiben.
Nie Argumente an Makros übergeben, die nicht geklammert sind.
Das ist, wenn wir es geklammert übergeben,
ist Klammertextuell eingesetzt wird gar kein Problem,
dann haben wir keine Probleme mit der Operatorpräzedenz.
Also im Favorit trifft einen der ständig,
wenn man da mit Makros arbeitet
Okay, wir möchten jetzt in dem Fall mal unsere Makros verwenden,
um den stromorientierten Programmierstil. Also die Sprache lese ich nochmal
neu zu erfinden, ja. Also bei der Sprache Lesi,
das haben wir ja zu Anfang des Semesters gesehen,
als wir über vorgezogene Auswertung versus verzögerte Auswertung diskutiert haben,
dass wir das sehr gut verwenden können,
um Funktionen zu definieren, die nicht strikt sind.
Kurze Erinnerung,
die Vokabel sollte sitzen,
was heißt Striktheit
Hm. Das ist eine meiner zweitbeliebtesten Klausurfragen, wann ist
eine Funktion strickt? Also sie sollten, eine Funktion ist
strikt, wenn sie nur für definierte Argumente Werte liefert.
Ja, wenn der Funktion, die nicht strikt sind,
liefern uns auch dann ein Resultat, wenn wir für Teile der Argumente undefinierte Werte übergeben haben.
Okay, und dafür ist eben die Verzögerte Ausfährtung genial,
weil wir, weil undefinierte Argumente nicht stören,
wenn sie nicht gebraucht werden
Und wir können auch unnötige Berechnungen vermeiden,
bei der vorgezogenen Auswertung war ja das Problem,
Argumente kommen an, wir stürzen uns eifrig drauf,
werten alle aus, ja,
und verwenden dann 20 Stunden Rechenzeit auf ein Argument,
was hinterher gar nicht benötigt wird.
Und deshalb verzögerte Auswertung, verweidet das. Und auch das
Wichtigste, aber auch im funktionalen Programmierstil potenziell unendliche Datenstrukturen.
Dass wir mal eben eine Rekursion über die Menge der
natürlichen Zahlen schreiben, ja, liest sich gut,
sieht klar aus
Und wenn wir vom Resultat nur die Dinge abfragen,
die wir hinterher wirklich brauchen, dann macht es nichts,
dass der Code es beschreibt, für jede natürliche Zahl,
die je gebraucht wird.
Es wird nur ausgerechnet für die, die wir brauchen.
Und es ist eben Listen als Ströme implementieren wie in Miranda,
Häskel oder in den Pipes, ne?
Gucken wir mal auf die Grundidee der Stromorientierten Programmierung. Wenn wir ein Programm im Programmierstil stromorientiert schreiben,
dann haben wir die folgenden Bestandteile, zum einen Generatoren,
Das sind Datenquellen, die uns potenziell unendliche Ströme von Werten liefern,
aus dieser Quelle wird, in dem es iterativ läuft,
einen Wert nach dem anderen produziert.
Und diesen Strom können wir verarbeiten in einer Weise,
wie wir es von Listen gewohnt sind, mit Mapping,
Filtern und so weiter.
All das, was auf einer Liste funktioniert, funktioniert auch auf Strömen.
Wir haben Transformatoren, in die eine Liste auf einen Strom
, auf einen anderen Strom abbilden.
Und wir haben Kombinatoren, die Ströme
Parallele Ströme verbinden zu einem neuen, wo eben kombinierte Werte drin sind.
Und Selektoren, Konstrukturen, um solche Ströme zu produzieren.
Also ein Beispiel, über das wir uns vorstellen könnten,
wir haben hier einen Strom, der erzeugt Frauen,
einen Strom von Frauen, ja,
so sie an der Eva und kommt immer wieder einer weiter nach.
Wir haben einen weiteren Generator, der erzeugt einen Strom von Männern
, so. Dann haben wir einen Transformator,
der nimmt jeweils das vordere Element des Stroms von Frauen und
Strom von Männern und kombiniert es zu einem Ehepaar.
Haben wir in den nächsten Transformationen,
die erzeugt daraus noch Kinder in dem Haushalt und so weiter.
Das heißt, hier haben wir Transformatoren, Kombinatoren. Ja,
so an Strom können wir uns vorstellen, die Listen.
Sie haben eine rekursive Struktur. Ein Strom hat immer ein
erstes Element und einen Rest. Ja, der Kopf,
der ist endlich. Ja. Und hat potenziell unendlichen
Reststrom oder sie können eben leer sein.
Also wir können Ströme unendlich lang definieren, ja
Aber die Kombinaturen und alles, was darauf arbeitet,
arbeitet immer auf dem ersten bekannten Element und der Rest kann eben noch undefiniert sein.
Und damit ist immer nur ein endlicher Teil eines Stroms ausgewertet.
Der Rest vom Strom, das ist ein Versprechen.
Wenn ihr denn wirklich was hier haben wollt,
na gut, dann gucken wir mal rein und versuchen,
das Element noch zu berechnen.
Ja, aber der Rest ist immer potenziell unausgewertet und wartet drauf,
dass irgendein anderer Ausdruck die Elemente aus diesem Rest braucht
Damit man mit Strömen arbeiten kann, müssen wir sämtliche Funktionen,
die wir verwenden, möglich als Stromfunktion schreiben.
Und Stromfunktion, das sind solche Sachen wie Maps zum Beispiel,
die immer das erste Element abbilden und für den Rest rekursiv sagen,
mache ich, wenn es denn gebraucht wird, ne?
Stromfunktion ist eine Funktion, die einen Eingabestrom auf den Ausgangsstrom abbildet.
Dabei werden Eingabewerte elementweise verarbeitet, ohne dass der Rest
des Eingabestroms ausgewertet werden muss.
Und damit das klappt, definieren wir
eine Stromfunktion in Kopfform
Also eine Funktion ist in Kopfform definiert,
wenn das Kopfstück des Eingabestroms nur vom Kopfstück des Oscarbestroms abhängt.
Also wenn wir praktisch das erste Element der Eingabeliste sehen und
rechnen können, daraus das erste Element der Ausgabeliste sehen,
ohne den Rest angucken zu müssen.
Diese Kopfform oder als Zwang für die Kopfform ist der Grund,
weshalb Programmiersprachen wie Miranda und Teskel,
bei denen jede Liste automatisch implizit ein Strom ist.
Warum diese Sprachen Entrekursion nicht unterstützen
Warum steht dies hier im Konflikt mit Entrekursionen? Erinnern wir
uns dran. Bei Entrekursionen haben wir ganz bewusst das so geschrieben,
dass die, weil der Rückgabewährt ist rekursiven Aufrufs,
das erst die ganze Berechnung auf den rekursiven Aufrufverband.
Und deshalb können Stromfunktionen damit nicht funktionieren, weil man überhaupt
erst ein Ergebnis sieht, wenn die Liste abgearbeitet ist.
Und das dauert ein bisschen über die Liste der natürlichen Zahlen
Also all unsere endrekursiven Fassungen sahen so aus,
dass der Wert zurückkommt. Denn die Eingabeliste bis zur Leanliste
abgearbeitet ist. In der Stromorientierten Programmierung ist es typisch,
dass wir eine Rekursion aufsetzen, weil der nie der Punkt erreicht wird,
wo ein Abbruch bei der Leanliste beginnt.
Und deshalb würden die für den Oscar des Strom erst ein,
wenn es ein Trikot sieht, erst einen Wert zurückgeben können,
wenn wir die Liste komplett abgearbeitet haben.
Das ist also nicht
die Kopfform
Wir können uns das am Beispiel nochmal angucken.
Ganz typisch für Endrekursionen würden wir die Funktion Map,
wenn wir sie schreiben wollen, so schreiben.
Ja. Map, irgendeine Funktion über eine Liste Access,
macht das folgende. Wenn die Liste Access leer ist,
dann nehmen wir unseren Akkumulator, drehen ihn um und geben ihn zurück.
Ein wichtiger Punkt bei Endrekursiven Funktionen, aha,
bin ich laut der Sprechwort, das Bohren auch lauter.
Interessant. Also der wichtige Punkt bei Endrekursiven Funktionen war ja,
Erstens für Trick geben das Ergebnis erst zurück,
wenn die Liste leer ist.
Und zweitens, weil wir immer alles auf den Akku schmeißen müssen,
wir am Ende die Liste umdrehen, weil alles verkehrt drum drinsteht.
Deshalb hier das Reverse. Im Arztzweig, da wird die
Liste rekursiv abgearbeitet. Die Funktion wird angewendet,
indem das erste Element vor den Akku geschmissen wird hier,
mit dem Cons vor den Akku.
Und dann wird es rekursiv für den Rest aufgelefen.
Aber es wird das erste Element nicht direkt als Ergebnis verarbeitet zurückgegeben.
Ja, in Kopfform ist es,
wenn wir eine allgemeine lineare Rekursion machen
Also einfach aufhören, wenn die Liste leer ist und im anderen Fall,
wenden wir die Funktion F für aufs erste Element
an und rufen es dann rekursiv für den Rest auf.
Das ist eine typische Kopfform. Wir können, ja,
mir einen Strom, das nächste Element ist schon klar.
Das ist das, was du mit Konst davor gehängt haben.
Das können wir zurückgeben. Und die Auswertung vom Rest können wir
verzögern, so lange, bis da was gebraucht wird.
Und für das Verzögern Hier, um zu sagen, okay,
die sind rekursiven Aufruf, den mach mal nicht bitte nicht gleich,
sondern erst, wenn jemand das braucht, ja,
dafür haben wir in Recket hier zwei schöne Sprachelemente.
Das eine ist das Delay. Das legt zu berechtigen ausdruckende Datenstruktur
an, ja. Also jemand beantragt was. Hier steht
der Ausdruck und wir sagen, da müssen wir nicht gleich machen,
ist niemand da, der das Ergebnis haben will.
Wenden wir die Special Form Delay drauf an. Ja,
wir werten es nicht aus, sondern dann nimm es verzögert
und das Ding tun wir in unser Archiv.
Und erst, wenn jemand das Ergebnis davon haben will,
dann werten wir es aus und wir werten es aus,
indem wir die Funktion Forst drauf anwenden.
Und das wertet es so weit aus,
bis in der Struktur das nächste Gillet kommt,
dann hört es wieder auf und wartet auf den nächsten Antrag,
dass was geschieht.
So ein Strom, das ist jetzt wie eine Liste,
eine linear rekursive Datenstruktur. Sie kann leer sein,
sie kann aus einem Kopf bestehen und aus einem Schwanz,
oder Titel oder wie auch immer.
Und der Rest hier, der Schwanz, der ist wieder
vom Typ Strom. Und der kann Dieser Strom hier,
der kannst du jedem Zeitpunkt nur endlich weit ausgewertet sein,
aber eben potenziell unendlich viele Elemente haben.
So etwas bekommen wir in der Sprache Lesy, wenn wir da
eine Liste definieren, ist sie automatisch genau so ein Strom.
Da müssen wir uns gar nicht drum kümmern.
Aber wenn wir es hier von Hand zum Üben mal selbst
programmieren wollen, dann haben wir jetzt folgenden Dinge.
Wir können ein Konstrukt brauchen, ein Konstrukt dafür,
einen leeren Strom. Wir haben eine Funktion Constream,
die nimmt ein erstes Element und einen Reststrom,
macht daraus einen neuen Strom
Die Akzessoren, Zugriffsfunktion, das ist das erste Element des Stroms gibt.
Und eine, die uns den Reststrom gibt. Und die sorgt auch
automatisch dafür, dass ein Teil davon ausgewertet wird. Und dann,
um in den Strom reingreifen zu können und zu erzwingen,
er soll bis zum zehnten Element ausgewertet werden,
können wir Stream-Ref verwenden mit dem Index i und sagt,
gib uns das Element.
Wir können jetzt unsere Ströme implementieren, einfach mit dem Dev
Macro, was wir schon kennen.
Konntest du ihm
Ein erstes Element und ein Reststrom. Und was macht er?
Erzeugt eine Liste. Und die hat als erstes Wort hier das Wort Cons.
Da wird das erste Element in den Strom eingehängt,
wird hier eingesetzt. Hier ist wieder das Anquot. Und
dann kommt aber Aufruf von Delay. Und der Rest,
den wir hier übergeben haben, der wird mit Delay übergeben.
Um das erste Element zu bekommen, holen wir uns aus
dem Strom einfach mit K das erste Element raus.
Um den Reststrom zu bekommen, wenn der Strom null ist,
gehen wir die leere Liste zurück
Wenn der Reststrom leer ist, geben wir
die leere Liste zurück. Wenn es ein Paar ist,
dann geben wir den Rest zurück und in allen anderen Fällen,
zwingen wir mit Force die Auswertung des Restes vom Strom.
Bin der Strom leer, der Test, ob der Strom leer ist,
da fragen wir einfach, ob die funktional erfüllt ist.
Und die Embdistream gibt uns hier einfach eine leere Liste.
Wir sehen hier das Wechselspiel von Dulay und Force.
Wenn wir einen Strom aufbauen, wird immer das,
was potenziell und endlich sein kann,
mit Dulay eingehüllt und abgelegt
Wird nicht ausgewertet. Mit Forst erzwingen wir es, wenn jemand
da reingucken will. Und damit können wir jetzt zum Beispiel
den Strom der natürlichen Zahlen ab der Zahl N erzeugen.
Also Inteachers from N, ab der Zahl N. Wir würden sagen,
die erste Zahl des Strumpfs, der mit N beginnt,
ist N. Und dahinter kommt mit Delay Inteachers from N,
aber wir beginnen bei 1 plus N.
Und wenn wir jetzt hier den Strom der Zahlen ab 3 erzeugen wollen,
Aufruf Inteachers from N mit 3,
dann bekommen wir eine Liste zurück und die besteht aus der Zahl 3.
Das ist das erste Element. Das ist schon da.
Und hier hinten, da sehen wir sowas komisches, Struct Promise.
Das ist die Regnete Repräsentation für ein Versprechen. Ja?
Das ist genau das, was ein Dilem macht.
Es erzeugt ein Objekt vom Typ Versprechen. Also wenn du willst,
dass ich es auswerte, mache ich es, aber du brauchst einen Force.
Ja, du brauchst Gewalt. So,
und was wir hier sehen, das ist eben das,
was wir in Lese jederzeit auch machen können.
Wenn wir eine Rekursion aufsetzen über die natürlichen Zahlen,
dann machen wir es nicht rückwärts von einer Zahl enden,
bis wir bei null angekommen sind, so wie man es im normalen Programmierstil mit vorgezogener Auswertung macht.
Nein, wir beginnen beim Startelement, ein Verankerungselement,
eins oder null, ja,
oder einem bestimmten Element und machen die Rekursion gen unendlich.
Ja, wir höhen bei jedem rekursiven Aufruf die Zahl M um eins.
Also der Strom ist damit potenziell unendlich lang.
Wir haben eine Rekursion über natürliche Zahlen ohne Abbruchbedingung
Und die Rekursion gilt über Nachfolger, nicht über einen Vorgänger.
Das schauen wir uns mal an. Am Beispiel Siebtes ihrer Tostines
für Printzahlen. Haben sie bestimmt in der Schule geübt,
wie finden wir Prim-Zahlen mit dem Siebtes ihrer Tostines, ne?
So typische Anfängeraufgabe. Idee ist, wir stellen uns hier
vor, auf eine endlosen Liste, alle natürlichen Zahlen.
Und dann nehmen wir die erste Prim-Zahl,
die wir kennen und streichen alle vielfach in dieser Prim-Zahl aus.
Also wir haben hier die Liste der Zahlen 1 und die erste Zahl,
die wir treffen, wenn wir noch nicht gesiebt haben,
wieder eine Primzahl und die Vielfachen von zwei sprechen wir aus.
Also vier, sechs und so weiter. Was nach diesem
Sieben mit dem Ausstreichen der Vielfachen von zwei übrig bleibt,
das erste Element davon ist wieder eine Primzahl.
Das möchte ich drei. Und von der streichen wir jetzt
auch die Vielfachen aus. Wenn wir dann wieder reingucken,
das erste Element, was dann übrig bleibt,
ist wieder eine Primzahl und so weiter und so kriegen wir auf diese Weise,
liste aller Primzahlen raus.
Also die Idee in diesem Schema,
Folge der Natürlichen Zahlen außer eins,
bildet einen Strom von Kandidaten
für Primzahlen
Kopf des aktuellen Kandidatenstroms, ist eine Primzahl.
Können wir wegnehmen und sagen, das ist eine Primzahl.
Wir nehmen diese Primzahl, sieben den Reststrom,
in dem wir alle Vielfachen der Primzahl rausschmeißen.
Dann nehmen wir den gesiebten Reststrom als neuen Kandidatenstrom und rekursiv
immer wieder das erste Element nehmen, damit den Reststrom filtern.
Und wir bekommen Strom von Primzahl aus den Köpfen unseres Kandidatenstroms.
Im ersten Schritt also haben wir zwei als Bremenzahl.
Das sieben wir dann, wir tragen das hier ein,
als Ergebnisliste, die hatte zwei.
Mit zwei sieben wir das
Da bringen wir nach dem Sieben raus,
3 ist Primzahl, nehmen wir zum Ergebnis,
kriegen hier raus, 5 ist Primzahl, 7,
wieder, vielfach von 5 und so weiter.
Und das schreiben wir jetzt mal als kleines Programm.
Define, unteilbar, X durch Y, ja.
X ist un, es ist nicht teilbar durch Y,
wenn der Divisionsrest von X gegen Y gleich null ist.
So, mit diesem Not Devisible können wir loslegen.
Unsere Funktion Siebel, einen Strom, Was für übergeben ist
ein Strom von, in der Schallmenge der natürlichen Zahlen?
Der Sieben guckt als erstes, was ist jetzt das erste Element?
Und gibt das als Resultat zurück in Kopfform?
Wir haben hier das typische Cons für einen Resultatsstrom in Kopfform.
Das erste Element des Eingabestroms ist das erste Element des Ausgabestroms.
Dass die Zahl, die vorne steht, nach dem Sieben ist eine Primzahl.
So, und das haben wir hier jetzt gelöst.
Und dann machen wir einen Delay und sagen,
verzögere bitte, der unendliche Strom soll nicht
ausgewertet werden
Was passiert, wenn wir einen Filter des Stroms machen,
einen Curry-Gang der Funktion nicht teilbar, vom Kopfstrom,
vom Kopf des Stromes, das erste Element, was wir haben,
das nehmen wir und damit filtern wir den Rest.
Hier rufen wir rekursiv, tief auf,
über den Rest vom Strom, des Stromes Strom.
Und wir filtern eben mit dem ersten Element. Und jetzt können wir sagen,
unsere Priebzahlen bekommen wir, indem wir alle Primzahlen ab der Zahl 2 nehmen,
den Strom der unendlich vielen Primzahlen ab der Zahl 2 und den sieben wir,
wie hier oben beschrieben, rekursiv.
Ja? Dann haben wir mit einem Rutsch alle unendlich in vielen Sprintzahlen potenziell berechnet. So,
wenn wir jetzt eine Primzahl haben wollen,
dann nehmen wir neue Funktionen Takestream, die erzwingt jetzt die Auswertung,
nämlich die nimmt die ersten zehn Zahlen, dessen,
was hier zurückkommt und ab dann wird echt gerechnet, ja?
Und dann sehen wir die ersten zehn Primzahlen. Der Rest
im Till Stream, der hat hier eine lange Kaskade von
Von den Delays, der wird nicht ausgewertet. Also wie
hoch wir hier auch gehen, das bestimmt wie weit,
dass der Algorithmus tatsächlich läuft und uns dann die Primzahlen liefert.
Genau das können wir mit der Sprache Lesi machen.
Das ist das, was in Record fertig drin ist,
im Record-System, ja, Dr. Racket.
Wir würden Natz-Up-End schreiben, ein Kunz von Natz-Up-N,
plus N minus eins, wenn wir die Sprache Lesi nehmen,
dann müssen wir uns nicht mehr um das Delay und Force kümmern.
Das ist einen gebaut, ne? Aber das macht
genau das. Also das hier ist der Zwinger Auswertung,
Doppeldoppel, das macht einen Fonds und wertet das hier aus.
Und dann bekommen wir hier tatsächlich zurück. Hier,
die ersten zehn Elemente der natürlichen Zahlen ab eins.
Ja? Und in Lesy geschrieben würde unsere Stromfunktion hier.
So aussehen, siebe den Strom, mach ein Kunst vom
ersten Element des Stromes, mache einen Filter.
Also unsere bekannten Funktionen höherer Ordnung, Map,
Filter und so weiter, die funktionieren in Lesy, wie Stromfunktionen,
ja, und und diese Force ist automatisch ein.
Ja, wenn Numen nicht teilbar ist, durch das erste Element,
dann wird der Rest des Stroms gesiebt. Und unsere Primzahlen,
die feinen Prime-Seafs, natürliche Zahlen ab zwei,
mit dem Doppel-Doppel, Force Force hier, take 10 Primes,
bekommen wir hier unsere 10 ersten Primzahlen.
Ich weiß nicht, ob Sie sich erinnern,
so in der Mitte des Semesters hatten wir
als erstes Beispiel fürs Arbeiten mit Symbolen
eine Sprache eingeführt
The man on the table with the green wall and the
man with zuck the mummeln on the table und so weiter.
Also wir hatten eine Grammatik geschrieben, die bestand darin,
dass wir jede Regel der Parkus-Nauer-Form einfach als Funktion geschrieben haben.
Und ein kleines Problem war eben, dass diese Grammatik in der Form,
wo wir Adjektive eingeführt hatten, potenziell unendliche Sätze erzeugen kann.
Wenn wir das Ganze in der Sprache lese ich schreiben,
also in dem Teil Dialekt Lesi von Dr. Racket,
dann können wir unbesorgt dieses Generate Oral aufrufen
Und damit potenziell unendliche Sätze erzeugen. Also, wir haben hier eine Phrase,
hatten unsere schöne Grammatik, die solche Sachen macht.
Und dann können, wenn wir die Phrasen alle als Ströme zeigen,
dann können wir mit Generate All sämtliche Phrasen erzeugen, sämtliche Restphrasen.
Und in der Grammatik nach der Frage suchen und dann
einen Strom erzeugen,
der potenziell
alle Sätze erzeugt
Skript habe ich das ausführlich beschrieben. Werde da
nochmal reingucken könnte, kann sich das mal angucken.
Wir haben noch eine Viertelstunde und die möchte ich gerne darauf verwenden.
Auf den Teil, den sie brauchen, um das Aufgabenblatt,
was ich verteilt habe, vollständig lösen zu können.
Dieser Teil soll einfach ein bisschen erleuchten, wann muss man
eigentlich oder sollte man den funktionalen Programmierstil verlassen und doch lieber mit Zuständen arbeiten.
Und wie kann man das effizient machen in einer funktionalen Programmiersprache
Die funktionale Programmiersprache hat zwei Zwecke. Das eine ist,
dass man mit Bezugstransparenz zugesichert, operationale Semantik und Denotationale Semantik zusammenfallend hat.
Und das erlaubt einem, dass man Programme automatisch transformiert. Ich finde,
das, was wir Denotationell hinschreiben können als Zusicherung,
was ein Stück Kot tut, wenn wir uns darauf verlassen,
dass das genauso ausgeführt wird, ja, operational,
dann können wir Programme schreiben,
die Kot transformieren und neue Programme daraus ableiten
Bezugstransparenz geht nur, solange wir nicht imperativ sind.
Manchmal aber sind bestimmte Dinge einfach, ehrlich gesagt, Schwachsinn.
Wenn man das im reinen funktionalen Programmierstil macht, nehmen wir an,
wir haben eine Liste von einer Million Werten und irgendwo da
drin brauchen wir ein Element ausgetauscht für eine neue Liste.
Was jede funktionale Programmiersprache macht, ist,
dass es die neue Liste erzeugt, indem diese Millionen Elemente
kopiert werden und dann beim Kopieren das eine Element ausgetauscht.
Und wenn das in der Rekursion geschieht,
wie wir zum Beispiel beim Appent gesehen haben,
dann erzeugen wir damit Ein Programm,
was im Wesentlichen Garbage erzeugt und ständig Konzenzellen anfordert und wieder auf den Müll wirft.
Und für solche Zwecke, wo man sich selbst beweisen kann,
dass das Programm korrekt bleibt, wenn man einfach in der Eingabeliste,
die man hat, ein Element austauscht.
Und dann mit der ausgetauschten Liste weiterarbeitet. Da kann man
es denn auch tun. Ja, man muss aber dafür Sorge tragen,
dass man beweisen kann, dass kein anderer Chor tatsächlich die Liste verwendet.
Und ein paar Beispiele, wie man effizient sein kann,
ohne gleich den Zum imperativen Programmierstil zu greifen, immer hier.
Das eine ist, die Methode von Cashing und Memoisation, einfach zum Hindern von Mehrfachberechnungen, Jan. Also beim Cashing ist es so,
dass wir Programme schreiben, die Werte, die mehrfach gebraucht werden,
nicht immer gleich in lokale Variable bindet,
sondern man kann es auch so tun, dass man Werte,
die man schon mal berechnet hat, sich einfach merkt,
eine Tabelle anlegt, in der man reinschreibt, X-Huch,
3.000 für das Argument X ist, wird reingeschrieben,
muss das nächste Mal nicht wieder berechnet werden.
Das heißt, anstatt den Code damit vollzukleistern,
dass wir für Zwischenergebnisse laufen, lokale Variable definieren,
verwenden wir eine Methode,
dass der Computer das für uns macht und wir uns darauf verlassen können,
dass nix mehrfach berechnet wird, wenn wir schon einmal berechnet haben,
ohne dass wir gleich für mehrfach Berechnungen ständig den Code mit lokalen Variablen belasten.
Und Memoisation, das ist die Technik der Memo-Funktion,
Gegenstand der Übungsaufgabe, die Sie haben,
dass einfach ein Stück Programmkurt genommen wird, wenn wir eine Funktion haben,
dass die automatisch so umgeformt wird in eine Funktion,
die sich alle berechneten Werte merkt in einer Tabelle.
Sodass wir, wenn eine Funktion aufgerufen wird,
nicht einen wirklichen Funktionsaufruf haben, sondern einfach aus dem Cash
das Ergebnis für ein schon bekanntes Argument genommen wird.
Und diese Umformung sollte automatisch sein, dass wir uns
gar nicht darum kümmern müssen.
Also wir hatten als abschreckendes Beispiel für eine Rekursion,
die man nie so programmieren sollte
Zum Beispiel die Fibonacci-Zahlen gesehen. Sie sind hier deklarativ so definiert,
wie sie mathematisch definiert sind. Die Ente Fibonacci-Zahl ist
die Summe der N-1-Essen und der N mit minus zweiten.
Und wir haben hier eine Baumrekursion und hatten auch gesehen,
Mensch, schon bei relativ kleinen Werten von N haben wir ziemlich viele Mehrfachberechnungen.
Wenn du die fünfte Fibonacci-Zahl haben wollen, in dieser Baumrekursion,
wird die dritte zweimal berechnet, die zweite dreimal,
die erste Fibonacci-Zahl wird viermal, ne fünfmal berechnet,
also durch die Baumrekursionen entstehen hier,
weil der Aufruf für ein Kipp von N,
N minus 1 nochmal berechnet und N den Wert für N minus 2 nochmal berechnet und der von N plus 1 berechnet den von N nochmal und den von und so weiter.
Es ist ein baumartiger Mehrfachbrechenaufwand. Wir hatten es gelöst zu Anfang,
indem wir einfach FIP transformiert haben in eine Entrekursive Fassung,
die einigermaßen schnell war, mit der Memo-Technik können wir uns das transformieren,
ersparen,
sondern ist Recket überlassen
Die Idee ist die folgende. Wir wandeln eine Funktion in eine Memo-Funktion
um. Wir legen eine Tabelle an für schon berechnete Funktionswerte.
Wenn die Funktionen aufgerufen wird, dann prüft,
soll die Funktion einfach nachgucken, ob sie den Wert schon kennt,
wenn sie ihn schon kennt, rechnet sie nicht neu,
sondern gibt einfach den aus der Tabelle zurück.
Wenn nein, dann berechnet sie Zähne knirschen, mal diesen Wert,
aber tut ihn in ihre Tabelle und muss ihn nie wieder berechnen.
Und dann wird noch dafür gesorgt, dass diese neue Funktion,
die diesen Komfort hat, aufgerufen wird,
wenn wir eigentlich die Alte meinen.
Das heißt, wir tragen den Namen der Funktion in
die Symboltabelle ein
Die Idee, also in unsere, für den Kern einer Memo-Funktion,
wir führen eine Tabelle ein, das ist ein Aufruf von Mick Table,
da müssen wir uns eine Datenstruktur überlegen, ich empfehle dafür eigentlich,
dass man eine Hash-Tabelle nimmt, ja.
Wir haben eine Funktions-Store, wir können eine Tabelle ablegen,
eine Funktion Retrieve, die kann die Tabelle auslesen, Konstrukte für die Tabelle,
ja, und dann ist hier der Kern des Ganzen,
wenn eine Funktion aufgerufen wird mit einem bestimmten Argument,
wird als erstes geguckt, ob ein gespeicherter Wert existiert.
Wir rufen einfach Retrief auf und versuchen festzustellen, gibt es schon einen Wert.
Wenn er existiert, geben wir den gespeicherten Wert zurück.
Andernfalls wird dieser Wert gespeichert und das Resultat ist dann der Wert des gespeicherten Wertes.
Also machen wir einmal damit eine neue Funktion Memo,
Funktion höherer Ordnung, die bekommt irgendeine Funktion und soll sie
in eine Memo-Funktion wandeln,
die nie dasselbe zweimal ausrechnet
So, hier kommt unser Letrick. Der hat zunächst,
dann haben wir eine leere Tabelle. Die Funktion Store.
Ich mache das mal hier einfach über eine Assoc-Liste. Ja,
aber wie gesagt, weil wir Test-Tabellen nicht besprochen haben,
aber Recket hat eine wunderbare Hash-Funktion. Also nehmen Sie,
wenn Sie es im echten Leben brauchen, an der Stelle ein Hash.
Hier wird also einfach in die Tabelle ein Paar aus Argument und Wert
eingetragen. Und wir haben eine Assoziationsliste, destruktiv,
sucht er drin mit Assoc. Und fertig. So,
wenn wir jetzt unsere Funktion Memo wandeln wollen, in eine Memo-Funktion,
Dann sagen wir hier einfach, Memo, Phipp, ja?
Das definieren wir, indem wir die Funktion für Bonachit-Zahlen an Memo übergeben.
Wenn wir jetzt Memo Fipp von 3 aufrufen,
kriegen wir den Wert 3 Zurückrücktritte für Bonachit-Zahl.
Beim ersten Aufruf muss leider, wie viel tatsächlich rechnen, ja?
Ein Fipp von 3 wird aufgerufen, es sieht,
aha, ich muss die zweite und die erste addieren.
Also die werden jetzt auch rekursiv aufgerufen, kommt eins zurück,
kommt eins zurück, kommt zwei zurück und dann unser Wert 3.
Wenn wir es wieder aufrufen mit 3, Dann wird,
sehen wir im Tres, es wird überhaupt nichts mehr gerechnet,
es kommt sofort der Wert zurück, den wir haben wollten.
Was aber noch ein kleines Problem ist,
gucken wir mal hierhin, die rekursiven Aufrufe, wir sehen,
dass FIP von 1 hier zweimal aufgerufen wurde.
Warum? Weil in der Art, wie wir Memofit definiert haben,
besteht Memofit, aber darin, dass hier die Funktion FN,
die übergeben wurde, aufgerufen wird.
Wir erzeugen eine neue Funktion Memorphip. Die kann sich alles merken,
aber das Interne ruft das alte Phipp auf,
was ich nichts merken kann.
Und deshalb haben wir hier dieses kleine Problem bei mir nochmal genau hingucken.
Es gibt von eins für zweimal aufgerufen. Nicht das Memorphipp.
So, und jetzt greifen wir hier tatsächlich zum Holzhammer.
Und das ist bei der ganzen Memor-Geschichte die eine Stelle,
an der wir imperativ werden.
Na gut, die zweite war das Eintragen in der Tabelle
Da muss wirklich ein kritischer Eingriff in die Bezugstransparenz ist.
Wir sagen hier jetzt einfach, das Symbol FIP wird umdefiniert
zur Mimo-Funktion von FIP. Das heißt, überall,
wo früher die Funktion FIP verwendet wurde, wird jetzt die Funktion Memofip aufgerufen.
Und wenn wir jetzt einen Trice machen,
dann sehen wir, bei FIP von 4,
wo ist FIP von 3 Aufrufen von 2 von 1.
Wir haben eine lineare Rekursion, dann kommt der Wert 1 zurück.
FIP von 0 wird aufgerufen, kriegen wir auch
den Wert von
Und das wird hier zusammengebaut und wir haben die fünf.
Alle rekursiven Aufrufe gehen jetzt zu der,
durch diesen kleinen Kunstgriff, zu der Mimo-Funktion.
Wir haben schlichtweg in der Symboltabelle von Recket den Wert geändert.
Wenn wir jetzt Tipp von acht aufrufen, sehen wir,
okay, das muss nur für sieben, für sechs,
für fünf aufrufen.
Und ab fünf hat es ja schon alles gerechnet.
Von den vorherigen Aufrufen von FIP sind alle Werte da.
Also wir haben eine lineare Rekursion bis zur ersten bekannten Zahl
Und wenn wir jetzt mal eine Laufzeitmessung machen mit und ohne Mimo,
dann sieht man hier bei den recht hohen Zeilen ohne Memo,
haben wir hier ungefähr 90 Sekunden.
Wenn wir das für die 40. Fibonetti-Zahl aufrufen,
sind wir hier noch im Bereich von Billy-Sekunden, ne?
Ist kaum messbar. Für 100 habe ich es abgebrochen und
gar nicht zu Ende gerechnet für FIP von N ohne Mimo,
aber mit sind wir da bei 20 Sekunden,
Und wir können es mit Mimo-Technik selbst verfippen von 1000 Aufrufen ohne große Probleme.
Das heißt, die junge Technik war verwandelt,
unsere Baumrekursion in eine lineare Rekursion.
Okay. Wo haben wir hierzu? Imperativen Sprachelementen gegriffen,
einmal beim Speichern in der Tabelle. Ja,
die Funktion Store hat mit Set Bang die Tabelle verändert und
die Referenz auf die Funktionen selbst mit beiden Maßnahmen
zusammen haben wir eine Funktion von exponentieller Ordnung in eine von
lineare Ordnung gewandelt
Okay. Wenn wir jetzt uns noch mal angucken. Und unser Pro-Log,
ich wollte noch mal an die Stelle gehen,
wie es das letzte Mal nicht angesprochen hatte,
wenn sie mit dem Pro-Log-System selbst üben wollen, was ich ihnen ans Herz lege,
um einfach ein Gefühl dafür zu bekommen, wie relationales Programmieren geht.
Sie finden in Pro-Log, in Recket,
in die folgenden und der Pakete einmal die Datenbasis,
da stehen praktisch alle Zusicherungen drin, wo eben drin steht,
Gift und Haus und was weiß ich alles, unsere Relationen, ja.
Sie finden den Code für die Unifikation in UnifyS, für die Interaktion an Backtracking,
das Raumprogramm ist das Brülluck SS und Prolog in Scream,
das heißt inzwischen Prolog in Recket, das ist ein Paket,
das alle Module bindet und all das finden sie in der SE3-Bib in Prolog, ja.
Was wir in Prolog verwendet hatten, ich gucke hier nochmal. Ich wollte immer die Stellen finden,
wo wir eine, wo wir Makros eingeführt haben,
also Was für eingeführt haben in ProLog, im ProLog,
in Recket möglich zu machen, das waren im Wesentlichen zwei Matros,
nämlich dass einmal dieses Assert ist ein Makro und das Gegenstück mit dem Fragezeichen hier vorne,
was die Anfragen macht, auch das ist ein Makro.
Das sind die beiden einzigen Makros, die verwendet wurden,
um die Spracherweiterung für ProLog in Recket möglich zu machen.
Also nur als Beleg, Frugal sein mit der Zahl der Makros,
die man einführt, um einen neuen Programmierstil einzuführen.
Der Rest sind Funktionen und keine Spracherweiterung,
also wirklich nur diese beiden neuen Sprachelemente wurden mit DevMakro eingeführt.
Okay, damit möchte ich für heute schließen.
Danke für die Aufmerksamkeit. Es wird kein neues Aufgabenblatt geben,
sondern für die nächste Woche verwenden,
probieren Sie einfach bitte sämtliche Probeklausuren durch,
die ich erstens in Stine stehen habe und mehr probeklausurt
finden Sie in meinem Lehreblock für diese Vorleser.