Und so gesehen, dass man Rekursionen recht einfach denen kann.
Ich möchte heute ein bisschen den Unterschied zwischen den rekursiven Definitionen,
den Berechnungsprozessen, die sie auslösen, betonen,
wo wir eventuell, sind Fallen laufen können, die dazu führen,
dass Dinge nicht berechnet werden können oder ungeahnten Aufwand verursachen.
In dem Zusammenhang auch kurz auf Korrektheit eingehen.
Und dann wird ab nächster Woche das Thema sein,
dass wir eigentlich zu dem Teil von Brecket kommen,
der mir am meisten Spaß macht, Funktionen höherer Ordnung,
Funktionen als Parameter verwenden, Funktionen als Ergebnisse verwenden.
Also erst mal heute, Rekursion und Induktion.
Wir haben eine ganze Reihe Rekursionen schon in den Aufgaben verwendet,
nicht einfach, damit sie da in die Gänge kommen.
Aber wann verwendet man es eigentlich? Also der Hauptzweck ist
nicht Beschäftigungstherapie in den funktionalen Programmierungsübungen, sondern wir verwenden es dann,
wenn wir damit Probleme adäquat lösen können.
Und im Allgemeinen immer dann, wenn wir vor der Frage
stehen, Mensch, wie haben wir ein schwieriges Problem?
Ich sehe nicht, wie ich damit zurechtkomme,
aber einen Teil davon kann ich lösen
Und dann versucht man, den Teil,
den man selbst lösen kann, explizit rauszuschreiben und das,
was man auf anheblich sieht, in den rekursiven Aufruf zu stecken.
Wenn man das gut macht und da ein Verfahren gefunden hat,
was auch konvergiert und irgendwann terminiert, dann hat man gewonnen.
Also schwierige Probleme durch eine Folge von Vereinfachungen, auf einfachere Probleme zurückführen.
Das ist eigentlich das, wofür wir es prozedural,
wenn es um die Berechnungsprozesse geht, verwenden.
Wir verwenden es aber auch, wenn wir Datenstrukturen haben,
wo wir uns nicht festlegen wollen auf die Größe.
Und potenziell unendliche Datenstrukturen darstellen wollen. Das geht sehr schön,
indem man den Teil, den man direkt zugreifen kann,
beschreibt, den Rest wieder in die Rekursive definition, die Datenstruktur steckt.
Verarbeitung rekursiver Datenstrukturen nimmt unsere Datenstrukturen rekursiv beschrieben,
bleibt es meistens gar nichts anderes übrig,
als für die Zugriffsfunktionen auch rekursiv zu werden.
Und das wichtige Thema heute, die Definitionen, die wir schreiben,
bei denen wir aufs Beste hoffen und den Prozessen,
die sie auslösen.
Gut, was bedeutet das vor Trikotsiv überhaupt?
Also nur, auch das mal zu definieren.
Eine Definition ist rekursiv, wenn sie auf den Grate
zu definierenden Begriff Bezug nimmt
Und das kommt aus dem lateinischen Wort Rekoriere, also zurücklaufen,
ja. Norm, das, was hier unten steht,
gilt für die Sprache Recket, ja,
eine definitiv rekursive Definition, muss immer einen trivialen Fall enthalten,
der nicht mehr auf die Definition Bezug nimmt.
Wir haben in den Übungen, die wir diese Woche besprochen haben,
schon gesehen im Zusammenhang mit Lesi, muss nicht so sein,
wenn wir verzögerte Auswertung haben, ja.
Da kann es sein, dass wir den trivialen Fallen die brauchen.
Und dann können wir ihn auch weglassen. Gut,
wenn wir beweisen wollen, ob etwas, was wir rekursiv geschrieben haben,
korrekt ist, terminiert, ob es eine Fälle erschöpft,
dann bilden wir das Problem meistens auf die natürlichen Zahlen ab.
Und deshalb schauen wir uns noch mal kurz an,
was sie aus dem Schulunterricht eigentlich schon kennen sollten.
Die Menge der natürlichen Zahlen ist rekursiv definiert durch die pianischen Aktionen.
Eins ist die natürliche Zahl,
wenn wir es mit natürlichen Zeilen zu tun haben,
dann ist eins immer so ein natürlicher Rekursionserbrochene oder eine Verankerung.
Jede Zahl hat einen bestimmten Nachfolger aus der Menge der natürlichen Zahlen.
Es gibt keine Zahlen mit dem Nachfolger 1. Und wenn wir zwei identische Nachfolger haben,
können wir daraus schließen, dass die Vorgänger identisch sind.
Das ist auch ganz praktisch. So,
und die Rekursion als Methode, um alles zu beschreiben,
weil sie hat jetzt genau auf diesem fünften Teil jede Menge von natürlichen Zahlen,
welche die Zahl 1 und zu jeder Zahl A auch den Nachfolger Strich enthält,
enthält alle natürlichen Zahlen.
Auch noch mal eine Frage an die Regie. Ich glaube,
das hält unglaublich, können wir den Touren einen Chick leiser machen?
Jede Prüfung gab es. Das ist,
im Moment ist es, fallen da echt die Ohren ab.
Da kann keiner erholsam schlafen. Das ist, glaube ich,
so. Test geht das so? Jetzt hören sie mich
gar nicht mehr, ne? Doch, gut. Okay
. Okay, ja, versuchen wir es mal so.
Das scheint ganz gut zu gehen
Also, wenn wir Inklusion schreiben als Lösungsschema,
vertrauen wir eben darauf, wir beschreiben alles für die Nachfolger.
Und wir haben es für die Verankerung beschrieben und
dann soll es hoffentlich für alle natürlichen Zahlen gehen.
Jetzt basierend und ostgehend von den pianischen Aktionen können wir eine
ganze Menge unterschiedlicher Varianten von Rekursionen, rekursiven Funktionen oder der rekursiven Definitionen aufstellen.
Und je nachdem, welche wir am Wickel haben,
kann der Berechnungsaufwand immens sein oder ein Selbstgänger.
Das Schönste, was einen treffen kann, ist die Minimalrekursive Definition.
Wenn wir das als definierende Gleichung schreiben,
automatisch gesehen und auf der rechten Seite steht nur ein elementarer
Fall und eine einzige rekursive Verwendung der Funktionen und sonst weiter nichts.
Dann haben wir das Beste, was einem passieren kann,
die minimale Rekursion. Zum Beispiel Beschreiben der Addition.
Das ist die typische minimale Rekursion. Wir haben den trivialen Fall,
wenn Y, wir wollen zwar Zahlen X und Y addieren,
wenn Y gleich Null ist, sei das Ergebnis X,
ja, muss nichts dazu addiert werden.
Und in anderen Fällen machen wir scheibchenweise, ja,
wir addieren immer eins dazu, als wenn wir davon ausgehen,
wir sind in einer Welt,
wo wir die natürlichen Zahlen durch die Nachfolger modellieren,
also zum Nachfolger plus eins kommen wir immer.
Ja, wir addieren eins dazu. Und von Y
ziehen wir eins ab, um zum Vorgänger zu kommen.
Und irgendwann ist Y hoffentlich gleich null. Unter welchen Bedingungen
können wir den Fall Y gleich null hier verpassen?
Ja? Das ist soweit gefallen und natürlich ein Zweifel,
sondern selten um eine negative Zeit. Genau,
also wenn sie sowas machen und darauf vertrauen,
dass sie bei der Abbruchbedingung in der Rekursion etwas mit Gleichtreffen,
dann würde ich immer versuchen, deine Abfrage einzubauen,
um zu testen,
ob man diesen Gleichfall nicht überspringen kann
Wenn wir uns das entrecket, jetzt dann mit einem Triss anschauen,
dann sehen wir dir minimalrekursive Rekursionen, ist wirklich super.
Wir rufen hier auf, Adire 3 und 4,
ja, im ersten Schritt kommen wir hier,
wenn wir Tres eingeschaltet haben mit Ricraya Record Trace und sagen,
wir wollen die Funktion add verfolgen.
Kommen wir, bekommen wir also den Aufruf vermeldet,
3 und 4, dann verringern wir Y um 1, erhöhen
X um 1, verringern Y um 1 und so weiter.
Und wir sehen die ganzen Aufrufe und am Ende kriegen wir
sieben zurück. Was Sie hier sehen sollten, ist,
dass die Rekursion wirklich beendet ist und fertig ist,
im Fall der Minimalrekursiven Rekursion, wenn wir hier das Ergebnis haben,
dann sehen wir im Drehs nicht mal,
dass noch irgendwas aus dem Stack genommen wird und verarbeitet wird.
Ja, diese Art von Rekursion ist einfach eine schlichte Iteration.
Wir haben linear Schritt für Schritt,
iterieren über die Zahl X und sind am Ende fertig.
Also durch eine minimale Rekursion kommen wir salopp und lässig jede Iteration ersetzen
und haben dadurch dann auch keinen Rückkehren aus dem Stack.
Eine allgemeine Klindejahre-Rekursion kann ein bisschen aufwendiger werden. Also wenn wir
Den Fall haben, dass wir auf der rechten Seite unserer Definition,
wenn wir etwas definieren. In jeder Fall Unterscheidung,
selbst nur eine einfache Verwendung haben und keine Einschränkungen haben,
dafür, was noch geschehen kann mit dem Ergebnis.
Dann sind wir bei der Allgemeinen linearen Rekursion. Also hier
wird das Att aufgerufen und mit dem Ergebnis von Atthurd nichts mehr gemacht.
Das wird als Ergebnis zurückgegeben. Hier bei der allgemeinen linearen Rekursion,
da wird dieses Ergebnis, was für einen rekursiven Aufruf von MyMap zurückkommt,
noch mit dem Konz versehen und mit dem geschieht etwas.
Und das ist Etwas, was das Ganze teuer macht. Diese Aufgabe hier,
das ist eine selbstgeschriebene Variante der Mapping-Funktion, was soll sie tun,
wenn wir eine Liste von Elementen, Access haben,
wollen wir auf jedes Element der Liste eine Funktion anwenden und die Liste mit den Funktionsanwendungen zurückkriegen.
Wie machen wir das, wenn die Liste leer ist,
sind wir fertig, dann gibt es nichts,
worauf wir was anwenden könnten, in allen anderen Fällen,
sagen wir, na gut, Liste ist lang,
wir kümmern uns mal nur ums erste Element,
wenden aufs erste Element, Kauf von XSF an und das,
was hier zurückkommt, setzen wir mit Cons vor das,
was zurückkommt,
wenn Veriko sief den Rest der Liste verarbeiten.
Ja, das ist das typische Schema einer allgemeinen Rekursion über eine Liste.
Wir tun ein bisschen was mit dem ersten Element und
den Rest hüllen wir ein in den Rekursiven Aufruf.
So, und dieses Cons,
was hier auf das Ergebnis des Rekursiven Aufrufs angewendet wird,
ist etwas, was ein Tick teuer ist.
Das sehen wir, wenn wir einen Dress machen.
Wir sind hier nämlich nicht fertig,
wenn wir bei der leeren Liste angekommen ist.
Jetzt muss die Rekursion rangehen und aus dem Aufrufsdeck
die ganzen Aufrufe vorher rausholen und all diese können sie
es noch machen
Also was passiert hier? Wir sehen mein Map,
wir sollen mein Map anwenden für die Quadratwurzel aus 1,4,9,16.
Können wir nicht. Wir sagen, na gut,
machen wir es fürs erste Element, hier dieses Kons, ja?
Und das können wir aber erstmal nicht tun, weil wir,
das sollen wir ja von dem Rest der Liste hängen,
also rufen wir die Liste rekursiv auf.
Wir sehen jetzt die rekursiven Aufrufe für den Rest.
Und erst sind wir bei der Lernliste angekommen sind,
können wir nach und nach diese Konses für die Anwendung aufs erste Element der jeweiligen Liste anwenden.
Und dann kriegen wir auf dem Rückweg sozusagen,
wenn wir wieder auftauchen, Ja,
dann tauchen wir mit der verarbeiteten Liste im Mund auf und haben das Ergebnis gefunden.
Das heißt, der Unterschied zwischen der minimalen Rekursion,
die wir vorher hatten, und der allgemeinen linearen Rekursion ist,
dass wir hier den Speicherplatz vorsehen müssen für den Aufrufsdeck und der
ist so lang und so tief wie unsere Liste lang ist.
Ja, es ist proportional zur Länge der Liste. Okay,
wenn wir uns das Dress hier mal anschauen,
dann sehen wir hier eben auch noch die Zwischenaufrufe,
ja, wo die ganzen Ergebnisse kommen,
wenn wir das querrot auch noch mitverfolgen,
ja.
Gucken wir uns mal typische Fälle für solche linearen Rekursionen an.
Wenn wir über natürliche Zahlen die Rekursion haben,
dann setzen wir eben nach den pianischen Aktionen bei der Verankerung an.
Der Ernst ist eine natürliche Zahl oder null,
wenn wir das Ganze um eins verschoben betrachten.
Und wir sagen, wie wir den Nachfolger bekommen. Also,
dass wir in der Rekursion irgendwie eine Funktion auf das Element
anwenden und darauf eine andere Funktion und sagen,
damit kriegen wir das F von N plus 1.
Der elementare Fall, wenn wir über Listen gehen,
ist die Lehreliste und die Rekursion setzt dann immer in
der Zusammensetzung mit dem Kunstoperator ein
Wenn wir irgendwie über die Liste gehen, wir greifen aufs erste Element,
wir greifen den Rest der Liste zu mit Götter und dann mit Cons,
das erste mit dem Rest verbinden, dann sind wir da.
Ja, richtig, aufwendig sind vor allem die ganz allgemeinen Rekursionen,
nämlich die nicht linearen allgemeinen Rekursionen und dafür schauen wir uns jetzt auch nochmal
ein paar Muster an, weil das immer gut tut,
wenn man die auf einen Blick erkennt und weiß, was einem dann blüht.
Das, was mir am meisten die Buggy-Ärger gemacht hat,
das sind die indirekten Rekursionen, ja,
und die können einen unerwartet immer dann treffen, wenn es am meisten wehtut,
wenn man im Team irgendetwas macht, also typischer Fall,
man sitzt im Team an einem Projekt und jeder sieht,
oh, das ist ein bisschen schwierig für mich.
Aber das Nachbarteam hat in seinem Modul eine Funktion, die löst es für mich.
Ich löse mein Problem rekursiv und für die Teillösung
rufe ich das auf, was mein Team gemacht hat.
Das Team denkt umgekehrt, wow,
ist ganz schön schwierig für mich, aber in meinem anderen,
mein anderes Teammitglied schreibt doch gerade eine Funktion, die es löst.
Ich rufe Rekord, sief das auf.
Sie haben eine Rekursion verteilt über zwei Module,
Sie sehen das im anderen Modul nicht, dass es rekursiv ist.
Und das kann, das ist das,
was indirekte Rekursionen türkisch macht
Und dann, das kann teuer werden. Also wir haben einmal die Indirekte,
das ist die Rekursion, die man nicht offensichtlich sieht,
weil der rekursive Aufruf über eine zweite Funktion geschieht.
Oder eine dritte oder vierte. Baumartige Rekursion. Ja,
die können zu einem exponentiellen Verarbeitungsaufwand führen. Und geschachtelte Rekursion,
das ist die Tüpe überhaupt. Gucken wir uns mal eine
indirekte Rekursion an, wo man sie sieht. Ja,
es kann eben ihnen passieren, im Team,
in sich nicht ordentlich absprechen, sich darüber verständigen,
dass wenn jemand eine Funktion schreibt und die rekursiv ist,
sollte es in der Schnittstelle
zu sehen sein
Dokumentiert. Okay, hier die indirekte Rekursion zum Beispiel,
wir wollen definieren, ob eine Zahl gerade ist.
Das können wir machen, indem wir sagen,
eine Zahl ist gerade, wenn ihr Vorgänger und gerade ist.
So, und wenn wir jetzt sagen wir, okay,
die Funktion ungerade haben wir auch nicht, die definieren wir,
eine Zahl ist ungerade, wenn der Vorgänger gerade ist.
Und schwupps, haben wir hier eine indirekte Rekursion oder verschränkte Rekursion. Ja,
also zunächst mal, wenn X kleiner Null ist, sagen wir,
hören wir auf, dann fragen wir nach dem ist,
ob das absolut Betrag von X und gerade ist.
Wenn X gleich 0 ist, dann sei es falsch.
Ja, null ist keine ungerade Zahl. Und in allen
anderen Fällen fragen wir, ob der Vorgänger von X gerade ist.
So, die Funktion gerade definieren wir,
indem wir fragen, was mit X gleich 0 ist.
Das sagen wir, sei gerade. Und in allen
anderen Fällen fragen wir, ob der Vorgänger Ungeräude ist.
Und wir haben hier schön die verschränkte, die indirekte Rekursion.
Also wir sehen es auch im Trest.
Die beiden rufen sich gegenseitig immer wieder auf und am Ende
erfahren wir jetzt immer
die Abbruchbedingungen nicht verpasst haben
Dass in diesem Fall hat es zu einer, wenn wir uns das nochmal anschauen,
diese indirekte Rekursion hat zu einer lindigeren Rekursion geführt.
Und zusätzlich auch minimal, weil eben weder der Aufruf von Even
noch von Ott verknüpft werden muss mit irgendetwas.
Ach, die baumartige Rekursion. Wenn wir Datenstrukturen haben,
die sich baumartig verzweigen oder wenn wir ein sehr komplexes Suchtproblem haben,
wo wir in jedem Schritt immer wieder viele Alternativen untersuchen müssen,
dann führt es meistens dazu, dass wir eine baumartige Rekursion bekommen,
wo jeder rekursive Aufruf entweder zum Abbruch führt oder zu einem Aufruf von weiter,
von mehreren.
Also wenn wir in einer Fallunterscheidung mehrfach auf die Definition
Bezug nehmen, dann kriegen wir eine baumartige Rekursion.
Sieht hier übrigens mal als Beispiel für Bäume, die so aussehen
wie Informatiker sich Bäume vorstellt, die die Köcherbäume.
Ich habe lange gesucht, um überhaupt mal Bilder von echten
Bäumen zu finden, die wie ein informatischer Binärbaum aussehen.
Also in der Welt der echten Bäume bin ich nicht fündig geworden,
aber zum Beispiel die Köcherbäume, was eigentlich Liliengewächse sind,
die verzweigen sich so, wie es sich gehört,
in jeder Schicht immer darin verzweifelt.
Die sind wunderbar
Ja, ein typisches Beispiel für Baumrekorde, ein Prozess,
wie jetzt zum Beispiel die Fibonachi-Zahlen,
die verwendet werden um einen goldenen Schnitt zu beschreiben.
Da haben wir zwei Abbruchbedingungen, also wenn N gleich null ist,
sei es null, wenn N gleich eins ist,
sei es eins,
und in allen anderen Fällen nehmen wir die Summe aus dem Vorgänger und dem Vorvorgänger.
Und da hier genau sitzt die baumartige Rekursion an und
man sieht das schon bei kleinen Zahlen,
die Rekursion ist ganz schön tief wird
Ein Beispiel sind die Lehrbaum. Also Bäume sind
recht schwer darzustellen auf Folien. Das Problem ist,
dass die sehr schnell die Zahl der Möglichkeiten auseinandergeht und wir
das Planar auf dem Papier gar nicht darstellen können.
Deshalb ist sie jetzt reidimensional gemacht. Einfach damit wir sehen
können, wie schon nach wenigen Rekursionsaufrufen die Zahl der Zweige
ziemlich hoch wird
Und dasselbe noch mal, wenn wir sogar eine Dreifachverzweigung haben.
Dann geht es noch schneller, dass wir zu einem riesigen
dichten Gewürf von Möglichkeiten kommen, was durchgerechnet werden muss.
Nach wenigen Rekursionsschritten explodieren eigentlich bei einer Baumrekursion die Zahl
der Fälle, die wir untersuchen und abhandeln müssen.
Wenn der Verzweigungsgrad hoch ist, dann hat man Ziemlich schnellen Desaster.
Und das hat Theodor Storm schon in seinem Gedicht über die Mike-Kätzchen sehr süß beschrieben.
Ich weiß nicht, ob sie ganz in der Schulzeit daran erinnern.
Vergangenen Mai-Chap brachte meine Katze zur Welt sechs an der liebsten Kätzchen.
Also wir haben hier einen Verzweigungsgrad von sechs, ja.
Meinen Kätzchen, alle weißen mit schwarzen Schwänzchen für wahr.
Es war ein zierliches Wochenbettchen. Die Köchin aber,
Köchin sind grausam und Menschlichkeit wächst nicht in der Küche.
Die wollte von den sechsten fünf ertränken.
Fünf weiße schwarz geschwänzte meine Kätzchen ermorden wollte die verruchte Vive.
Ich half hier heim
Himmels segne mir meine Menschlichkeit. Die leben Kätzchen.
Sie wuchsen auf und schritten binnen kurzen, erhobenen Schwanz,
das ist über Hof und Herd.
Ach, wie die Köchin auch ingrimmig rein sah.
Sie wuchsen auf. Und nachts vor ihrem Fenster probierten sie
die allerliebsten Stimmchen. Ich aber, wie ich sie so
wachsen sah, ich priese mich selbst und meine Menschlichkeit.
Ein Jahr ging um. Und Katzen sind die Kätzchen.
Und Maitag ist. Wie soll ich es beschreiben,
das Schauspiel, das sich jetzt von mir entfaltet?
Mein ganzes Haus vom Keller, das zum Giebel an.
Jeder Winkel ist ein Wochenbettchen. Hier liegt das eine dort.
Das andere Kätzchen in Schränken, Korben und der Tisch und Treppen.
Die alte Garten. Nein, es ist dann aussprechlich,
liegt in der Köchin
jungfräulichen Bette
Und jede, jede von den sieben Katzen hat sieben,
denkt euch sieben junge Kätzchen, mein Kätzchen,
alle Weißen mit schwarzen Schwänzchen. Die Köchin rast,
ich kann in der Blindenwut nicht Schranken setzen dieses Frauenzimmers.
Ersäufen will sie alle 49. Mir selber, ach,
mir läuft der Kopf davon, um Menschlichkeit,
wie soll ich dich bewahren?
Was fange ich an mit 56 Katzen? Schönes Beispiel mit
hohem Baumverzweigungsgrad schon bei der Stufe 2 ist das Saster da.
Kann gute Poesie draus machen. Wir können es mal
einem anderen Beispiel mit hohem Verzweigungsgrad anschauen hier.
Ich habe hier mal ein Bild baumartig,
rekursive Struktur
Jedes Dreieck, ja? Da setzen wir wieder ein Dreieck dran.
Also in jedem Schritt machen wir aus jedem Dreieck drei Dreiecke, ja?
Und erste Isteration, da haben wir zwölf Linien, die
wir zeichnen müssen. Bei der zweiten haben wir schon 48,
bei der dritten 192, oh je, ja?
Fünf, sechs. Und da habe ich aufgegeben,
ich habe mal abgeschätzt, wie lange es dauern würde bei der CPU,
mit der ich damals gerechnet habe.
Also bis fünf haben wir es eben gesehen,
da hat mein MacBook Pro damals,
das war noch ein etwas langsameres, 124 Millisekunden gebraucht,
bei zehn Iterationen wäre ich bei zehn, bei zwei Minuten gewesen.
Bei 20 Iterationen, also sehen diesen gewaltigen Sprung, ja,
wäre ich bei vier Jahren gewesen. Und bei 36 Iterationen,
also so lange hält auch ein Rechner mit guter Abteilung nicht aus.
Das ist, die Zahlten werden also irgendwie gigantisch.
Sehr schnell, ne? Und die Iteration i bis 100,
ja, also das ist echt noch nichts Besonderes.
Gut, wir zeichnen wieder diese Schneeflocken, um das mal auszuprobieren.
Wir können in reckig ganz gut zeichnen,
das haben sie in den Beispielen auch schon gesehen,
wir können Bilder zusammensetzen, wir können Striche zeichnen.
Der Generator, den wir hier gesehen haben für diese kochische Schneeflocke,
der nimmt einfach eine Linie, von Seitenlinie des Dreiecks und drittelt sie.
Und das mittlere Drittel wird ersetzt durch dieses Dreieck,
wo wir bis zur Mitte nach oben gehen und dann wieder weiter.
Wir haben die Variablen fremden, bis du,
in der Mitte brauchen wir doch den Mittelpunkt und berechnen wir aus,
wie wir den Strich zeichnen.
Als rekursive Funktion geschrieben, Snowflake, Line in Reckett geschrieben.
Wir beginnen beim Punkt Fromm, das war der linke Tour,
ist der rechte. Wir sagen, bei der wievielten Iterationen
wir sind und welche Farbe wir zeichnen wollen.
Ich rufe auf die Funktion vor, Solid Line aus Recket,
wo wir von dem Übung From To2 in der Farbe Color einen Strich machen.
Und dann teilen wir es in vier Abschnitte auf.
Die Funktion in Japolet habe ich selbst geschrieben,
die berechnet den Punkt der Koordinaten zwischen diesen beiden.
Und dann muss noch die Spitze, die ist hier berechnet werden.
Wir müssen um die Höhe dieses Dreiecks nach oben
Okay, und das kriegen wir einfach,
indem wir den normalen Vektor zu der Verbindungslinie von Punkt Tromp,
zum Punkt Two nehmen und in die Richtung kleines Stück nach oben gehen.
Dann haben wir hier diesen Tipp. So,
und jetzt zeichnen wir, machen wir einen rekursiven Aufruf,
wo wir noch die Ergebnisse von Snowflake Line zurückgeben.
Das soll einfach mitzählen, wie oft wir iteriert haben,
ja. Snowflake Line zeichnet die Zeichnung und gibt zurück,
wie oft sie sich selbst rekursiv aufgerufen hat.
Ja. Die habe ich
mit Paphrey gemacht
Also, was wir sehen können, ist vor allem,
dass wir einen Prozess haben, der rechnet, ja.
Es dauert einfach. Und wenn wir viele haben,
können wir sie eben hier in Pafre zum Beispiel das Modell sehr oft verwenden und mehrfach einsetzen.
Okay, gucken wir nochmal auf die Arten der Rekursion, die wir jetzt kennen sollten. Wir hatten gesehen,
das, was am wenigsten wehtut, ist die Minimalrekursive,
das entspricht praktisch einer, schlichten Iterationen,
die lineare Rekursion, die ist auch noch linear,
aber die meiste Arbeit wird auf dem Rückweg gemacht und wir müssen Speicher verbrauchen,
in der proportional zur Länge der Iteration ist, ja,
das kann teuer werden.
Die indirekte Rekursion mit zwei Dingen haben sich gegenseitig aufrufen,
das kann gut gehen, kann eine schlichte,
minimale Rekursion werden, aber wenn die Teammitglieder nicht wissen,
dass sie sich rekursiv aufrufen, dann kann es Überraschungen geben,
ja, da können sie ungewollt eine Baumrekursion oder Sonstiges auslösen,
Baumrekursion ist sehr aufwendig,
Weil hängt vom Verzweigungsgrad der Bäume ab, wie schnell das Desaster kommt.
Bleibt noch die geschachtelte Rekursion. Ach so,
meine Versuche, Bäume in der Natur zu finden.
So sind wie ein Informatiker sich wünscht. Einfach Gabelung immer wieder
. In der Welt der Alten bin ich fündig geworden.
Also zum Beispiel hier die Gabelzunge. Dichotome Verzweigung nennen es die Biologen.
Wenn es wächst, verzweigt es sich in jedem Grad.
Und es ist wunderbar. Die Köcherbäume oder eine achtfache oder
sechsfache Rekursion können wir hier bei den Drachenbäumen bekommen.
Geschachtelte Rekursion. Das ist etwas, wo man vorsichtig sein
muss bei der Verwendung. Es gibt viele Fälle,
wir sehen auch im Skriptbeispiele,
wo die geschachtelte Rekursion sinnvoll ist und Probleme sehr gut beschreibt und effiziente Verfahren sind,
wenn man aber nicht aufpasst,
kannst du sehr großem Aufwand führen und so das abschreckende Beispiel überhaupt,
ist eine der Ackermann-Funktionen.
Ich habe hier mal diese Variante der Ackermannschen-Funktionen genommen, geschachtelte Rekursionen erkennen wir darauf,
dass auf Parameterpositionen als Akter Aktualparameter engrekursive Aufruf gegeben wird.
Also es ist nicht wie bei der Baumrekursion,
dass wir zwei Therme haben, gleichberechtigt in einem Aufruf,
die verknüpft werden, sondern die Rekursion bekommt den rekursiven Aufruf als Parameter mit.
Und damit haben wir ganz hohe, schnelle Wachstumsgrade für die Rekursion. Okay,
also ein Beispiel, wo wir die geschachtelte Rekursion,
die sind voll verwenden können,
ist zum Beispiel das Modul nur zweier Zahlen,
rekursiv zu berechnen,
um da schnell hinzukommen
Billige Art, es ist ja einfach die lineare Rekursion,
wir wollen A Modulo B rechnen, ja,
und wir ziehen so oft B von A ab,
bis der Rest kleiner als B geworden ist.
Damit hätten wir eine lineare Rekursion und die ist vom Aufwand her sehr groß,
proportional zur Größe des ersten Arguments. So, wenn wir sagen,
wir wollen mit die Weite und Konka in jedem Rekursionsschritt,
denn das Argument klein kriegen,
dann können wir es so machen, dass wir sagen,
Modulo von A und B ist A,
wenn das Bier schon mal kleiner ist,
Und das A schon mal kleiner ist als B. Ja,
dann haben wir es.
In allen anderen Fällen ziehen wir B von A ab und nehmen es Modulo B.
Ja, wenn A größer als B ist.
Und wir können aber auch noch beachten,
dass wenn wir ein Modulo gegenüber zweimal B machen,
dass wir das schneller zum Ziel kommen.
Also da sitzen uns die Wald- und Kunker an. Wir nehmen nicht Modulo B,
sondern zweimal B. Und dann gucken wir bei dem,
was rauskommt, ob wir da in den Bereich gekommen sind,
wo wir schon ein Ergebnis haben.
Das können wir machen, wenn A,
wo es ja zweimal B ist
Und diesen Aufruf von Modulo, Modulo A, zweimal B,
den geben wir hier als, übergeben wir hier als Parameter.
So, wenn wir uns hier den Triss anschauen,
das geht relativ schnell, also durch diese Schachtelung werden die
Argumente fürs Modulus schnell kleiner und wir kommen sehr schnell zum Ergebnis.
Gut, wir haben ja gesehen,
wenn wir jetzt über Korrektheit sprechen wollen,
dass wir im Wesentlichen versuchen,
es auf die perianischen Aktionen
mit zurückzuführen
Dazu brauchen wir eben eine Abbildung auf die natürlichen Zahlen.
Ja. Und das Prinzip der vollständigen Induktion, das kann
man in der Programmierung ganz gut in dieser Fassung verwenden.
Eine Aussage A von N sei richtig für die Zahl 1.
Beim Induktionsschritt, da zeigen wir,
dass die Induktionsannahme A von Kap für eine natürliche K,
Salka, stets die Richtigkeit für den Nachfolgern, Karstrich, beinhaltet.
Das heißt, wenn wir es zeigen können für einen natürliche Zahl,
dann können wir über die Definition des Nachfolgers auch die Korrektheit dafür zeigen.
Und der Induktionsschluss ist dann, weil es Wenn wir das zeigen können,
ist es für N richtig, dann gilt es auch für N plus 1.
Dann gilt es eben für alle natürlichen Zahlen, bei den natürlichen Zahlen so definiert sind.
Bei den linearen Rekursionen, da können wir es direkt anwenden,
wenn wir als Argument ohnehin natürliche Zahlen haben.
Und bei allen anderen Rekursionen versuchen wir, die rekursive Struktur
auf die natürlichen Zahlen abzubilden. Bei den Listen folgen rein,
bietet es sich einfach an, Positionen in der Liste,
in der Folge, in der Reihe zu nehmen.
Und dann gehen wir bei der Rekursion über zum Beispiel über die Länge der Liste, ne?
Wir nummerieren und zählen einfach die Elemente der Struktur auf,
über die wir iterieren, ja? So,
wenn wir jetzt mal zeigen wollen,
dass eine Funktion X hoch N terminiert, ja,
dann schreiben wir erst mal so ein X hoch Rennen,
hier in der billigsten Form, lineare Rekursion.
Wenn N gleich 0, salt es eins sein und wir
schauen auf unsere Definition, was ist denn die Potenten,
die Definitionen für die Potenzen von Zahlen stellen fest,
das ist richtig.
Das Ergebnis für N gleich 0 wird richtig sein,
weil N hoch 0 eben gleich eins ist
Und die Definitionen sagt, dass das XON,
das Produkt aus ExoN minus 1 mal X ist.
Und damit können wir jetzt sagen, ja,
wir stecken die Annahme rein, dass das hier richtig ist.
Die Multiplikation mit X wird auch richtig sein, wenn Recket
richtig implementiert ist. Also gilt es auch für den Induktionsschritt
. Um das jetzt ordentlich zu machen, führen wir,
um direkt auf die natürlichen Zahlen zu kommen, erstmal ein,
dass wir eine Induktion über eine Zahl Car machen,
die wir als N-minus eins definieren.
Wir sagen für K gleich N gleich minus eins ist N
gleich null und Power von X von null terminiert.
Da keine weitere Funktionsanwendung erfolgt. Für Tag gleich 1
und N gleich 2 wird Power von 1 auch terminieren.
Der Power von null von exterminiert, das folgt uns so weiter.
Damit haben wir es also geschafft und den Induktionsschritt auch geschafft. Gut,
bei der Länge der Listen würden wir einfach in dem Fall über mein Map,
was wir eingeführt hatten, sagen,
dass wir den Positionen in der Liste nehmen und da kommen wir zu einem ganz ähnlichen Schema,
das wollen wir jetzt mal nicht im Detail machen.
Aber wann terminiert eine Rekursion überhaupt? Also jeder Rekursionsschritt sollte
die Argumente Dichte an die Abbruchbedingungen bringen und wir
Listen müssen kürzer werden, Zahl, Argumente kleiner und wir müssen
die Abbruchbedingungen so formulieren, dass wir sie nicht verpassen.
Das ist eigentlich relativ, ja. Schauen wir uns nochmal
anderen lineare kursiven Prozess an. Also wir sehen in diesem Prozess,
in jedem rekursiven Aufruf wird das Argument um einen kleiner,
leicht verändert, aber es wird kleiner und irgendwann erreichen wir
die Abbruchbedingungen,
Gut, das Problem bei den rekursiven Prozessen ist nun mal,
dass sie nicht nur den Wert definieren, der zurückkommt,
so wenn wir jetzt in den internationaler Semantik haben und es hinschreiben,
sondern bis wir den Wert ermittelt haben,
muss das Reduktionsschema durchgeführt werden.
Und je nachdem, wie wir unsere Ausdrücke formuliert haben,
kann der Prozess, der dahin führt,
eben unterschiedlich aufwendig sein.
Also ein Problem hatte ich schon angesprochen bei dem,
was wir beim Unterschied zwischen
minimal rekursiv und allgemeiner Rekursion
Das ist eben diese Nachklapperei, wenn wir ein Rekursionsaufrufsdeck brauchen.
Also wenn wir hier die Länge einer Liste bestimmen,
indem wir sagen, okay,
die Lehre listet die Länge null und jede andere Liste hat
die Länge des Restes der Liste plus 1,
dann können wir diese ganzen Additionen eben erst ausführen,
wenn wir zum ersten Mal für den letzten Rest überhaupt eine Zahl bekriegt haben.
Deshalb muss das Ganze in dem Rekursionsdeck abgelegt werden und rausgeholt werden.
So, und das heißt, es kommt eben der Speicheraufwand
dazu, das Ablegen hinzuschreiben, Aufrufe zu speichern.
Wir müssen sie wieder rausholen und erfahren erst auf dem Rückweg
das Ergebnis. Das nennt sich nachklevern. Ja,
also wir haben einmal den Rechenaufwand, jeder Rekursionsschritt muss gemacht werden.
Na gut, damit das erwarten wir. Aber der Speicherplatz
ist, das kann uns tatsächlich in Teufels Küche bringen.
Und deshalb rate ich hier in unseren Übungen auch immer,
dass sie, wenn sie in Recke die Voreinstellung treffen,
wie viel Memory genommen werden soll.
Stände Memory ruhig kurz ein. Auf diese Weise erzwingen sie,
dass eine Rekursion, die dem Bach runtergeht,
relativ schnell abbricht,
weil der Speicher erschöpft ist hier
Gut. Also die zurückgestellten Berechnungen, und deshalb heißt es
im Nachklappern, werden umgekehrte Reihenfolgelast in First Out berechnet.
Und die werden typischerweise im Kellerspeicher abgelegt, ja.
Okay. Ein wichtiges, noch eine weitere wichtige Vokabel-Entrekursion.
Ein rekursiver Prozess, der sich das, der kein Nachklappern erzeugt.
Der heißt Entrekursiv, ja? Also wenn das Ergebnis
der Rekursion nicht mit anderen Termen verknüpft werden muss,
dann haben wir einen Entrekursiven Prozess.
Wir können es uns sparen, den Spalt,
den Stack anzulegen
Reckett und jede Sprache der Liz-Familie verspricht, dass wir
keinen Aufrufsdeck haben, wenn die Funktion endrekursiv formuliert ist.
Und die Prozesse nennen wir dann iterativ. Gut, also Reckett oder Kommenliss,
übersetzt der Compiler endrekursive Funktionen automatisch an eine echte Iteration.
Also sie haben von der Effizienz her keinen großen Unterschied,
als wenn sie das selber mit einer Vorschleife der Waldschleife geschrieben hätten, ja?
Sieht nur eleganter aus, ja?
Wenn wir jetzt mal die Länge einer Liste berechnen wollen,
ohne diese Nachklapperei, könnten wir das so machen.
Wir führen Was müssen wir erreichen? Gehen wir doch mal
hier hin. Was hat da für die Klapperei ausgelöst?
Moment. Das Problem ist hier, wir können die Addition erst ausführen,
wenn Mai längst zurückkehrt mit dem Ergebnis für den Rest der Liste.
Deshalb müssen wir eben das Plus-eins speichern und uns merken,
als noch zu erledigen. Wir müssen um Entrekursion zu erreichen,
dafür sorgen, dass dieser Aufruf,
der letzte Aufruf ist und alle Additionen auf Parameterposition geschehen.
Und dazu braucht man oft einen Akkumulator.
Es gibt Entrekursionen,
die ohne Akkumulator auskommen
Akkumulator ist wieder notwendig, noch hinreifend, für eine Endrekursion,
ja. Es ist eine Technik, die man nimmt,
um es zu erreichen, wenn einem nichts anderes einfällt.
Aber wenn sie in der Klausur gefragt werden sollten,
woran erkennen sie Endrekursion, sie kriegen keine Punkte dafür,
wenn sie sagen, da ist ein Akkumulator. Gut,
also die Länge einer Liste, wir sagen, es ist,
wir geben den Akkumulator über die Länge einfach errechnet haben,
zurück, wenn wir,
wenn die Liste leer ist und in allen anderen Fällen,
dann rufen wir es rekursiv auch für den Rest der Liste
und erhöhen den Akkumulator um eins
So, jetzt ist, wenn sie die Akkumulatortechnik verbinden,
gehört es dazu, dass sie da selbst dafür sorgen,
dass ein Akkumulator vernünftig initialisiert wird.
Das sollten sie nicht den Benutzern überlassen. Die könnten da sonst was reinschreiben.
Also wenn immer sie mit Akkumulator arbeiten in der Rekursion,
gehört es dazu, dass sie eine einhüllende Funktion schreiben,
die den Akkumulator verbirgt.
Der ist ja eine technisch, die sie eingeführt haben,
die dem Benutzer nichts angeht.
Also wir definieren hier im Mailens, wird nur das Argument,
XS übergeben und der Aufruf mit dem Akkumulator
ist hier verborgen
Und hier sehen wir jetzt im Stress tatsächlich,
also auf diese Methode, kriegen wir wunderbar.
Eine einfache, lineare Rekursion, die Entrekursiv ist.
Ja, mit dem letzten Aufruf ist es da.
Und wir sehen auch hier wieder Akkumulator hochgezielt wird.
Am Ende ist die Liste leer und der Akkumulator wird zurückgegeben.
Ja. Also einhüllende Funktionen sind wichtig.
Ihre Übungshelfer werden darauf herumreiten und Punkte abziehen,
solange bis sie alle Fug akkumulierenden Funktionen einhüllen.
Der Rechenaufwand für einen baumartig rekursiven Prozess,
der ist echt ein anderer Schnack und den schauen
wir uns auch jetzt bei den Fibanachi-Zahlen
nochmal ein
Jeder Aufruf von N führt zu zweien, es sei denn,
wir haben eine der Abbruchbedingungen am Wickel, ja.
Und was wir dabei auch ganz gut sehen können,
ist nicht nur, dass es ein ziemlicher Rechenaufwand ist,
sondern dass viele Aufrufe mehrfach gemacht werden.
Also das ist eine absolute Sauerei hier, ne?
Fipp von drei wird zweimal aufgerufen, Fipp von zwei
sogar dreimal, Fipp von eins, fünfmal.
Wir haben nicht nur dieses in die Breite gehen,
sondern auch noch unnötige Mehrfachberechnungen.
Wir werden am Ende des Semesters uns Verfahren überlegen,
wie wir Funktionen,
die Mehrfachberechnung in der Rekursion machen
Automatisch transformieren, in welche die das unterlassen. Und das
schon mal als Vorausschau, aber jetzt überlegen wir uns,
wie wir die Fibunachi-Zahlen in eine lineare Rekursion verwandeln können.
Wir wenden die Akkumulator-Technik um die Fibunachi-Zahlen einfacher zu machen.
Und dazu verwenden wir in diesem Fall einfach zwei Akkumulatoren.
Wir haben ja die Fibunachi-Zahlen so definiert, dass sie die Summe
der Vorgänger sind und diese Vorgänger übergeben wir mit als Argument.
Und damit können wir
Folgendes sagen
Kumulator 1 geben wir zurück, wenn es endgleich null ist,
sind Akkumulator 2, wenn N gleich eins ist.
Und in allen anderen Fällen rufen wir uns rekursiv damit auf,
dass wir den Akkumulator 2 an die Position Akkumulator 1
bringen und die Addition auf die Stelle verschieben,
wo wir den zweiten Akkumulator vorher hatten.
Da führen wir die Additionen durch Akkumulator 1 plus Akkumulator von zwei,
denn jetzt haben wir beide Vorhänge, beide Vorgänger am Wickel.
Also mit jedem Aufruf übergeben wir die beiden
zuletzt berechneten Fibonachi-Zahlen und das tun wir hier auf der Akkumulatorposition
Und wenn wir das jetzt aufrufen, dann sehen wir,
dass dieser kleine Kunstgriff mit zwei Akkumulatoren sogar eine Baumrekursion verwandelt hat in eine lineare Rekursion.
Entrekursiv. Ich habe schon über Platz für Stacks gesprochen.
Das ist natürlich, wenn wir viel mit Rekursionen arbeitet,
eigentlich das, wo wir wirklich viel Speicherplatz verbrauchen.
Wir verbrauchen Speicherplatz, aber auch beim Aufbauen von den dynamischen Datenstrukturen.
Zum Beispiel unsere Listen. Racket ist so benutzerfreundlich,
dass es eine Freispeicherverwaltung hat und wir nicht selbst dafür sorgen müssen,
dass wir ein Kunstzellen und all so ein Kram selbst entsorgen.
Ja, das macht Racket für uns im Hintergrund.
Diese Garbage-Collection, die hat aber einen kleinen Haken.
Also erstens kann man nicht bestellen, wann sie kommt,
sondern dann, wenn wir eventuell gerade Echtzeit-Animationen machen wollen,
dann kann die mit einmal die Müllabfuhr kommen und erstmal den ganzen Betrieb aufhalten,
indem sie unsere Kunstzellen einsammelt, ja.
Und damit ist natürlich die Echtzeit-Animation zum Teufel. Das Beste ist,
wir programmieren so, dass wir gar nicht erst Müll erzeugen,
also wie im realen Leben, es ist für die Haushaltskasse günstig,
wenn wir nicht für die Müllabfuhr bezahlen müssen,
sondern sparen, indem wir Müllvermeidung betreiben.
Und das ist recket nicht anders. Wann entsteht Müll?
Also nehmen wir mal an, wir definieren einen String.
Wenn wir einen String haben, muss der für Platz besorgt werden,
der Speicher adduziert werden, um Distinken zu schreiben.
Oder wenn wir eine Liste erzeugen, ziemlich viele Kunstzellen um diese Liste darzustellen.
Wenn wir das erzeugt haben und wir vernichten die Referenzen auf diese Sachen.
Also ich habe hier immer ganz bewusst gesagt, Set Bang,
an die Variable Garbage heißt jetzt weg damit,
ja, und die Garbage 2 auch weg.
Damit gibt es keine Möglichkeiten mehr im Programm auf diese beiden Datenstrukturen zuzugreifen.
Alle Referenzen sind weg. Und die Garbage Collection,
die eingebaut ist in Recket, merkt das,
dass sie Werte hat, auf die es keine Referenzen mehr gibt.
Wenn sie dann das sucht und fährt es weg, ne?
Gut, wann erzeugen wir Müll? Einige haben, glaube ich,
schon den Übungen von ihren Übungsselfern auf die Finger geklopft,
gekriegt, wenn sie in einer Rekursion eine Pent verwendet haben
Hier, weshalb? Ja? Wir haben zwei Listen A
und B. Ich habe hier eine Liste A, 1,
2, 3, eine Liste B, XYZ.
Da repräsentiert im Speicher durch diese Kunstzellen. Also hier haben wir diese Zellen,
die jeweils auf eins zeigen, auf 2,
auf 3 und dazwischen Referenzen auf die anderen Elemente.
Ja, das kostet Speicher. Eben, nehmen wir diese Zellen ablegen.
Wenn wir jetzt eine Liste A, B erzeugen wollen,
B hinter A gehängt, dann macht Vegget Folgendes.
Es setzt sich einfach hier einen Zeiler ein,
am Ende von A, der es auf B zeigt,
weil damit hätten wir zwar das richtige Ergebnis A mit B verbunden,
aber wir hätten die Liste A zerstört
Das geht also nicht. Wir hätten die Liste A modifiziert,
denn wenn jetzt jemand die Variable A abfragt,
geht die mit einmal hier mit X weiter.
Wollen wir nicht. Was macht Recket,
um zu verhindern, dass Werte verändert werden?
Das geht hin und erzeugt eine Kopie des ersten Argumentes.
Also merken sich bitte, wenn Sie mit Appent arbeiten,
zwei Listen mit Appent zu einer langen Liste verbinden,
Recket wird immer eine Kopie des ersten Element der ersten Liste erzeugen.
Und dann kann man nämlich hier das Ende der ersten in der Kopie
das Ende der ersten Liste auf die Liste B zeigen lassen.
B ist eingebaut,
aber jeder Aufruf von Appent
Pent erzeugt eine Kopie der ersten Liste, ja.
Und wenn wir das jetzt mal in einer Rekursion verwenden,
dann werden sie sehen, dass Pent in der Rekursion wirklich teuer ist.
Wir wollen, das können wir mal testen, indem wir eine eigene Funktion Reverse schreiben,
drehe eine Liste um, ja. Also wenn wir eine Liste haben,
die besteht aus dem Kopf der Liste und dahinter hängt eine Restliste,
das ist der Schwanz, ja,
wir wollen sie umdrehen mit Reverse,
dann können wir doch eine Liste machen,
indem wir den Rest der Liste,
den Schwanz hier nehmen,
ihn mit Reverse umdrehen,
rekursiv und dann
den Kopf dahinter setzen
Und das kann man ganz naiv so schreiben.
Meine eigene Implementation von Reverse besteht darin, ja,
wenn die Liste leer ist, gibt es nichts zu tun,
haben die leere Liste zurück.
Ansonsten drehe ich den Spann der Liste um, ja,
mit Mai Reverse, rekursiver Aufruf, vom Rest der Liste.
Und dahinter hänge ich mit, er pennt die Liste,
die ich bekomme, wenn ich nur den Kopf der Liste in eine Liste einfüge.
Ja, das Ergebnis ist zweifellos richtig, wie man hier sehen kann.
Umgedreht, der Schwanz der Liste vorn und dahinter der Kopf,
ja. Problem, das erste Argument von dem Appentier,
der rekursive Aufruf, Mai
Reverse könnt ihr von
Der wandert bei jedem Rekursionsschritt sofort wieder auf den Müll,
weil er gar nicht gebraucht wird.
Ja, im nächsten Schritt kriegen wir wieder eine Kopie und
wieder eine Kopie und wieder eine Kopie. Das heißt,
wir erzeugen, so lang wie die Liste ist,
so viele Kopien des Rechts der Liste, die sofort wieder verklappt werden.
Und das ist ziemlich viel in Müll.
Wenn wir mal hier die Liste im Mais, ja,
mit diesen vier Symbolen drin, Mayo Reverse, Mayo Reverse,
im ersten Schritt machen wir einen Appent von der leeren Liste und S.
Die Lehre wird kopiert, das tut nur nicht sonderlich weh.
Dann wird hier Dieser Teil, es kopiert,
dann wird der Teil Sieg kopiert, dann wird der Teil Sia kopiert.
Das heißt, das, was wir an Kopien kriegen,
wird in jedem Schritt auch noch länger.
Das heißt, wir haben für die Kopien einen Rechenaufwand,
der proportional zum Quadrat der Listenlänge ist.
Und alles für die Katz. Das können wir mal testen,
nehmen wir den Aufwand versuchen zu messen,
überzeugende Liste mit den natürlichen Zahlen bis zur Zahl N.
Und im ersten Schritt nehmen wir eine Liste mit 10.000 Zahlen
So, dieses Time Reverse, das ruft unser My Reverse
auch für die natürlichen Zahlen bis N. Für 10.000 Zahlen kriegen wir raus,
dass die CPU-Zeit hier in Sekunden 6.000 Sekunden ungefähr ist,
also 6.000, 6.000 Millisekunden, macht sechs Sekunden um eine Liste der Länge 10.000 umzudrehen.
Das ist schon ganz schön lange, ja. Und die
Garbage Collection von diesen 6.000 Millisekunden hat die Garbage Collection hier allein 4.000 gebraucht.
Also die meiste Zeit ging nicht ins Rechnen, sondern in das Müll wieder einsammeln.
Und wenn ich das jetzt für eine dreimal so lange Liste mache,
dann sehen wir, wird aus den 6.000,
wird hier ein 55.000. Das heißt,
der Rechenaufwand ist quadratisch in die Höhe gegangen.
Und wenn wir jetzt immer längere Listen nehmen und da die Garbage-Collection-Zeit messen,
also für eine Liste mit 20.000 Elementen, hätte ich getestet,
ein kommendes jetzt, nicht in Recket.
Recket kann ich die Garbage-Collection-Zeit nicht gut sehen.
Da hatte ich 20 Millionen Millisekunden, alleine ein Garbage-Collection,
um eine Liste von 20.000 Elementen umzudrehen.
Das ist ein Witz, ja
Also, 20.000 Millisekunden sind 20.000 Sekunden,
also da kann man recht lange drauf warten.
Also der Müllberg, den wir mit Appent bekommen, kann immens sein.
Gucken wir mal, wie wir es effizient hätten machen können.
Also, indem wir einen Akkumulator einführen, können wir das Ganze
sehr viel effizienter machen, indem wir Cons als Appent verwenden.
Also, merken Sie für sich mal so als Erinnerung,
wenn immer Sie in Versuchungen kommen,
das erste Element einer Liste vorne mit Appenta vorzusetzen,
Gehirn einschalten und sagen, nee, Konz ist besser.
Und das kann man sich am Umstapeln ganz gut klar machen.
Wie drehen wir eine Liste um? Wir können eine Liste
ganz einfach umdrehen, wie wir das erste Element nehmen,
es auf den Tisch legen, das nächste Neben drauflegen und so weiter.
Das geht automatisch mit dem Konz und wir haben automatisch
die umgedrehte Liste. Dass auch die Erklärung dafür,
wenn Sie eine Rekursion mit Cons machen,
dass meistens die Liste am Ende erstmal umgedreht ist.
Macht nichts. Die umgedrehte Liste und einmal Reverse aufrufen ist wirklich viel,
viel schneller, als wenn Sie die Rekursion mit Appent
machen,
um das Reverse zu vermeiden
Also unsere Öko-Variante von Reverse, mit einem Akkumulator.
Ganz einfach. Wenn die Liste leer ist,
geben wir den Akkumulator zurück, der enthält unsere umgedrehte Liste.
Und in allen anderen Fällen,
dann nehmen wir den Akku und setzen das erste Element davor,
kommt Kar für den Akku und rufen das Ganze mit
dem Rest der Liste als Argument auf.
Und schon sind wir fertig
Und hierran erkennt man aber eben auch ganz gut,
wenn wir diese Rekursionsstruktur haben, dass wir mit einem Konzbeginn,
irgendwas wird auf Akkumulator-Position von Element gesetzt.
Und dann kommt der Aufruf für den Rest,
kriegen wir immer ein umgekehrtes Ergebnis zurück.
Auf das wenden wir Reverse an Standardfunktionen von Recket
und wir haben das gute Ergebnis, ja.
Und jetzt zum Vergleich mal, wenn wir die Liste mit Reverse umdrehen.
Selbst wenn die Liste eine Million Elemente lang ist,
war die Anzeige der Garbage-Collection-Zeit immer noch null.
Die Appent-Variante hat ja fast eine Stunde gebraucht für das Gleiche
Das heißt, die CPU-Zeit ist wirklich nur aufs Rechnen
verwendet worden und die Garbage-Collection hatte echt nichts zu tun,
selbst mit einer 1-Millionen-Elemente-Langeliste.
Gut. Nochmal ein kleines Beispiel für Baumrekursion. Bringen wir
die Bäume auch noch mal für Blütenträume zum Blühen.
Der Turn fehlt schon wieder, ne?
Na gut. Stellen sich per Günste Morgen dazu vor.
Das war der Ton,
der jetzt kommen sollte
Ja. Okay, gut. Also, Bäume. So wünsche ich mir die Bäume.
Machen wir mal einen Safe, gut. Ja, also,
ich denke, wir haben jetzt schon die wichtigen Sachen,
die man einfach parat haben sollte,
wenn man in den Übungen jetzt drangeht, Rekursionen zu verwenden.
Zusammengefasst, suchen sie vor allem beim Nacharbeiten die verschiedenen Arten der Rekutionsschemata gut zu laden,
dass sie einer Funktion sofort auf den ersten Blick ansehen können,
welche Art von Rekursion vorliegt, kriegen wir auch noch Übungsaufgaben dazu.
Aber so in den Textaufgaben der Klausur,
wo man eben nicht programmiert, da könnte schon passieren,
dass sie gefragt werden,
was ist das eigentlich für eine Art von Rekursion und welchen Aufwand erwarten sie?
Also funktionale Programme in Reckets sind häufig rekursiv,
wenn wir Iterationen brauchen, Wiederholungen und so weiter.
Es gibt daneben aber eine zweite Welt in Recket in den funktionalen Programmiersprachen,
wo wir wieder Wiederholungen auf ganz andere Art machen,
nämlich durch vordefinierte Funktionen höherer Ordnung.
Und auf den Teil möchte ich jetzt zu sprechen kommen. Also Kontrolabstraktion,
indem wir einfach anstelle jetzt an der Rekursion zu machen,
lieber eine Funktion aufrufen, die die Funktion mehrfach anwendet.
Gut, was sind denn Funktionen höherer Ordnung?
Also eine Funktion ist eine Funktion höherer Ordnung,
wenn sie als Argument Funktionen bekommt oder wenn sie eine Funktion erzeugt und als Wert zurückgibt.
Diese zweite Art ist das, was wir sehr oft verwenden
werden, so ähnlich wie sie
Java mit Vererbung, Methoden verändern, indem sie ein bisschen dazu tun.
Werden wir in Reckett Funktionen nehmen, die schon ganz gut das tun,
was wir wollen, aber wir werden sie verändern,
eine neue Funktion daraus machen und auch ein bisschen dazu tun.
Und das andere, dort,
wo sie in Java hingehen und Iteratorklassen schreiben würden oder etwas,
dafür schreiben wir Funktionen höherer Ordnung, Funktionen,
die an Kontrollrahmen vorgeben und dann parametrisieren wir, mit Funktionen.
In Java würden sie an der gleichen Stelle ein Objekt übergeben,
das die Methode kennt, die ausgeführt werden soll.
Wir übergeben nicht die Objekte, wir übergeben gleich,
das wird getan werden soll. Aber ansonsten ist der Schema
sehr ähnlich. Und das machen wir eben über Funktionen höherer Ordnung.
Funktionen, die entweder Argumente bekommen. Und dann haben wir
einen Kontrollrahmen oder Funktionen, die Funktionen erzeugen,
die wir dann an andere Funktionen geben.
Und das ist das, wo wir praktisch Funktionen spezialisieren,
wie sie es sonst im Recket in Java durch Vererbung machen würden.
Also jede Funktion ist jedenfalls
entreckelten Wert
Vom Typ Prestigestadt, die hat einen wohl definierten Typ und
wir können sie eben als Argumente an andere Funktionen geben.
Also wenn wir sortieren wollen,
können wir einfach ein Sortierprädikat über in Ordnungsrelation übergeben,
als Prädikaten, eine Funktion,
die zwei Zahlen als Argument bekommt und dann sagt,
sind sie sortiert oder nicht.
Und dann haben wir hier dem Beispiel Aufruf, also Sord-Funktionen,
wir übergeben die Liste und sagen, sortier nach kleiner.
Oder wir können Funktionen als Wert zurückgeben. Also wenn wir
den Sinus aufrufen wollen, wenn X größer 3 ist.
Hinzu wird, könnten wir es so schreiben, mit Exposer
3 Sinus in anderen Fällen würden wir den Cousinus aufrufen.
Hier geben wir die Funktionen zurück. Verwenden Sie nicht auf irgendwas an,
sondern sagen, heute nimm den Sinus oder nimm den Cousinus.
Wir können Funktionen in Listen einbauen. Hier stehen die Funktionen
in der Liste drin. Und das können wir auch für überall
da, wo es ums grafische User-Interface geht, verwenden,
wenn wir Dinge zeichnen wollen.
Und so in Recket erzeugen wir praktisch.
Die interaktive Oberfläche nehmen wir Funktionen als Parameter verwenden
Also hier mal, wenn wir zeichnen wollen,
es gibt eine ganz simple Funktion, mit der wir
ein bisschen Funktionen darstellen können, die aus Graf fahren.
Der habe hier die Quadratwurzelfunktionsquerroot übergeben. Was heißen soll Graf fahren?
Zeichne mir bitte den Grafen dieser Funktion. Und ich habe
noch gesagt, in welcher Farbe, Red, ja.
Und dann habe ich dieses Bild hier gekriegt. Also mathematischer,
Mathematiker werden traurig sein und sagen, Himmelswillen,
die Achsen sind nicht beschriftet und so weiter. Gut,
es gibt auch Pakete, die da ein bisschen nobler sind,
aber für den ersten Beispiel, um einfach was zu zeichnen, ideal.
Wir sagen, Graf fahren übergeben die Funktion,
sie wird gezeichnet und sagen noch,
in welcher Farbe
Und häufig werden wir, wenn wir Funktionen übergeben,
die nicht groß definieren, indem wir das Kind taufen.
Hatten wir schon drüber gesprochen, als wir die Landernotation besprochen haben.
Wenn Sie einfach einen Ausdruck hinschreiben wollen, der gezeichnet werden soll
, können wir es mit Graf Fangzoom machen.
Graf Fang, zeichne bitte die Funktion,
die ein Argument X kriegt und Sinus von X-Mezinus von X-Mahl nimmt.
Das Ganze in Rot. Also hier haben wir eine
anonyme Funktion übergeben, Lamda. Wir haben ja nicht erst
den Namen Sinusquadrat gegeben. Wenn wir sie nur einmal brauchen,
lohnt es sich nicht
Oder wenn wir einen Unterschied zwischen Trunk Kid und Frauen sehen wollen,
ja, zeichnen wir hier in meinem Blaue, dass Trunk geht.
Und in lila des Round. Also das zum Beispiel für
Funktionen als Argument. Aber jetzt kommt, ich hoffe,
Sie sind noch richtig wach. Bei einem Beispiel,
wo wir mit Dreckett eine neue Funktion errechnen.
Also nicht selbst die Definition hinschreiben, sondern eine Funktion schreiben,
die bekommt eine Funktion und wir kriegen eine andere Funktion als
Ergebnis zurück,
die zur Laufzeit erzeugt wird
Und da nehmen wir mal ein Beispiel,
was sie zum Abi sicherlich ganz oft und intensiv geübt haben.
Wir berechnen die numerische Ableitung einer Funktion. Also, zu
jeder Funktion gibt es ihre Ableitungsfunktion. Und was wir jetzt machen wollen,
wir schreiben eine Funktion, die berechnet die Ableitung einer Funktion,
aber nicht den Wert der Ableitung, sondern sie gibt uns,
wenn wir eine Funktion F übergeben, kriegen wir F-Strich,
die Funktion der ersten Ableitung zurück.
Gut, wir machen das Ganze ein bisschen billig,
indem wir keine große Genauigkeit da reinnehmen,
aber einfach um das Schema
zu zeigen
Nehmen wir mal. Hier habe ich die Funktion x Quadrat gezeichnet, ja.
Und wir wollen jetzt die Funktion automatisch erzeugen, die für jedes,
jede beliebige Funktion an einer bestimmten Stelle uns den Funktionswert gibt.
Die erste Ableitung rennen wir uns, ist der Grenzwert,
den wir bekommen,
wenn wir hier einfach ein Delta Y durch ein Delta X teilen,
an einer Stelle, um hier die Steigung der Tangente rauszukriegen.
Und das machen wir jetzt in Recked mal ziemlich brutal,
indem wir dies die Y durch DX einfach approximieren,
durch ein kleines Delta Y dividiert durch ein kleines Delta
So. Unsere Funktion, die eine Funktion F bekommt, soll
eine neue Funktion zurückgeben. Und das sehen wir hier auch,
das ist sie geglückt, nämlich der Wert,
der zurückgegeben wird, beginnt mit dem Wort Lamda.
Wir bekommen hier die Funktion zurück, die kriegt ein Argument
X und berechnet. An der Stelle X, Folgendes,
den Funktionswert von X plus DX.
Gucken wir nochmal auf die Zeichen und Kiel.
Der Funktionswert von X wäre hier X plus D,
X wäre der Funktionswert an dieser Stelle
Und den teilen wir. Von dem subtrahieren wir den Funktionswert an
der Stelle F von X. Also damit kriegen wir unser Delta Y.
Und das Ganze teilen wir durch DX und DX habe ich
hier definiert, als eine ziemlich kleine Zahl, 0.0001.
Da könnten wir ein bisschen großzügiger sein, das vernünftig machen,
aber hier steht mir einfach so für den Start ein kleiner Schätzwert.
Das heißt, wir nehmen eine Funktion iF,
wenden einen Drift drauf an und kriegen eine neue Funktion zurück
und diese Funktion kriegt ein Argument X und rechnet aus,
er von Expos DX minus Ephan X geteilt durch DX
Also die Ableitung an der Stelle X-Rechnet. Und diese Funktion hier aus,
ja? Ja, rufen wir das Ganze mal auf.
Der Riff der Sinus-Funktion, ja? Das Kind taufen wir
Sinn, Appel. Ableitung der Sinus-Funktion. Und wenn wir
uns jetzt anschauen, was ist der Wert von Sinnen, Appeln?
Na gut, das ist dieser Lamdau-Ausdruck, der hier zurückgekommen ist.
Und da sagt uns Rechnet nur, wir haben eine Procedure zurückbekommen.
So, wenn ich diese Funktion nehme,
die wir hier gekriegt haben, und setze hier hin,
und rufe sie auf an der Stelle null,
dann kriegen wir raus 0.9999,
ist das eigentlich richtig?
Das,
was sie erwartet hätten
Wenn Sie an der Stelle null die Ableitung der Exfunktionen berechnen,
erinnert sich noch jemand, ah ja?
Warum eine glatte Eins? Das war so witzig. Ja.
Gut, die erste Ableitung der Sinus-Funktion ist ja die Cosinus-Funktion.
Und der Cosinus von an der Stelle null sollte exakt eins sein. Gut,
ich kann diesen Wert hier, der dichtbare Eins liegt,
Raket echt nicht übel nehmen mit der lappigen Genauigkeit,
die ich hier vorgegeben hatte
Wir liegen ein kleines bisschen daneben. Und damit ist also
ein Ergebnis in der Gegend von 0.9,99 gar nicht mal so schlecht.
So, und wenn ich, ja, das ist eine Funktion,
ja, ich kann die jetzt noch mal differenzieren mit zweiter Ableitung zu kriegen.
Also ich nehme die Funktion, sind Appel,
die mich hier rausgekriegt habe und differenziere sie noch mal durch Aufruf der Funktion, der Riff.
Und was ich jetzt zurückkriege, ist die Funktion der zweiten Ableitung, F-Strich.
Und wenn ich die Aufrufe an der Stelle null,
na, Ableitung der Kursinus-Funktion ist minus Sinus X,
dann ist das Ergebnis in der Gegend von minus 1
auch nicht schlecht
Gut, also, was wir hier als erstes gesehen haben,
ich hoffe, Sie wissen es zu schätzen, ist,
wir nehmen eine Funktion und machen daraus eine andere.
Wir kriegen eine neue Funktion zurück,
die wir auch zur Laufzeit gleich verwenden können, ja.
Und in diesem Fall ist es eben die Funktion der ersten Abteilung,
die können wir nochmal differenzieren und nochmal differenzieren.
Und ein bisschen testen und uns die Werte angucken.
Das können selbst im Interpret ja mal ausprobieren.
Was immer, der Riff ist eine typische Funktion,
höhere Ordnung, High Order Function auf Englisch.
Sie kriegt eine Funktion als Argument
Der berechnete Wert ist ebenfalls eine Funktion. Wir können
das Ganze wieder auf sich selbst anwenden und Ableitung,
der Ableitung, der Ableitung berechnen, ja.
Wir können das, was zurückkommt auf Argumente anwenden.
Wir können es für jede andere Funktion in Ausdrücken verwenden,
in Listen einbauen und so weiter. Und diese Funktionen höherer Ordnung,
mit der haben wir einen richtig guten eleganten Baukasten,
mit dem wir eigentlich die Programmabstraktion in Recket machen.
Die Kontrollabstraktionen, die Abläufe bestimmen, ja. Und die Funktionen höherer Ordnung,
die vordefiniert sind, sind praktisch die Idiome Programmiersprache,
die sollten wir verwenden, wenn wir bestimmte Muster haben,
die wir programmieren wollen, sonst versteht uns keiner, ja.
Also es ist die Pragmatik von Record. Wir haben Standardlösungen,
Funktionen, höherer Ordnung für viele Dinge, die immer wieder
vorkommen und die werden wir hier in der Vorlesung auch üben, ja.
Deshalb, weil eben das Beherrschen der Idiomüll zum Programmierstil gehört, ne?
Genauso, ja, wenn man Schach lernt, muss,
reicht es nicht, man lernt aus, was jeder Stein tun darf,
man muss eben die typischen Standardstellungen auch kennen.
Und das ist das, was wir in der Primatik lernen, ne?
Also halten sich bitte an die Standardlösungen, ja,
wir werden als wichtigste Funktion Map Filter und Fold kennenlernen.
Zum Beispiel als Funktion über Listen. Und wir erwarten,
dass wenn immer sie ein Problem genannt bekommen,
das programmiert werden soll, dass sie erkennen, aha,
das sind die Standardlösungen für diese Funktionen höherer Ordnung.
Also indem sie die Idioten betreffen, verwenden.
Und das Schöne an Standardlösungen ist,
da hat sich schon mal jemand Gedanken gemacht,
die Korrektheit zu beweisen, die Effizienz zu untersuchen und so weiter.
Macht es robuster. Okay. Jetzt beginnen wir mal
unseren Werkzeugkasten zu sammeln und all die Standardlösungen kennenzulernen,
die sie auf jeden Fall bis Weihnachten
gut beherrschen sollten
Fangen wir mal mit den Sachen an, die man auf Listen macht,
ja? Häufiges Problem zum Beispiel, da werden wir immer
zur Funktion Apply greifen. Nehmen wir mal an,
wir haben Zahlen, die sind einfach so,
sind gegeben, aber dummerweise als Liste, ja?
Und wir wollen diese Zahlen nehmen als Argument für eine Funktion.
Zum Beispiel das Maximum hier, ja? Wir könnten nicht schreiten,
die Maximumfunktion nimmt beliebig viele Zahlen als Argument,
aber wir können nicht die Liste mit den Werten reinschreiben.
Sondern wir müssen schreiben, Max und dann die Listen
schön flach als Argumente reinschreiben, Klammer
zu, wir können nicht einfach die Liste mit
den Argumenten reinschreiben
Und für diesen Fall, wenn wir das als Liste haben,
müssten wir schreiben, hier Max von K,
von Data, Max van Kada, von Data,
Max von Köder, der von Data und so weiter.
Das ist schrecklich, ja. Vor allem,
wenn wir nicht mal wüssten, wie lang die Liste ist.
Und dafür gibt es jetzt eben ein Brackett, die tolle Funktion Apply.
Die appliziert eine Funktion, die wir übergeben, auf die Elemente einer Liste.
Praktisch so, als hätten wir hier geschrieben,
die Funktion Max hier nach der ersten Klammer reingeschrieben, ja?
Also, wir können schreiben Apply, Max Data und dann kriegen wir sieben raus.
Es ist, als hätte man den Aufruf Max von Zwei,
drei, sieben und so weiter. Ja, oder Appley,
kleiner, ein, zwei, drei, vier.
Appley, kleiner gleich. Weil das geht,
wenn uns, wenn wir die Argumente haben als Liste zusammengebaut,
nehmen wir Appley applizieren, die Funktion auf die Liste.
Und das Problem ist gegessen, ja. Die Verträge,
über die wir gesprochen haben, das sind auch alles Funktionen höhere Ordnung.
Also wenn sie Zusicherung ins Programm schreiben wollen als Verträge,
an den Schnittstellen mittendrin und so weiter, ja,
dann sehen Sie schon, wenn wir hier zum Beispiel einen Kontrakt,
den wir definieren wollen und dann anwenden.
Dann haben wir ja den Konstrukt der Flat-Construct und der
kriegt als Argument tatsächlich Prädikate, die getestet werden sollen.
Und ListOff kriegt auch einen Kontrakt als Argument übergeben.
Also Funktionen gehen als Funktionen auch an dieser Schnittstelle,
wo wir über Verträge und Zusicherungen reden. Zum Beispiel,
wir wollen in ein Programm reinschreiben, dass die Liste,
die übergeben wird, eine sortierte Liste sein muss.
Dann könnten wir das so schreiben
Instruieren, einen neuen Vertrag, Flat Contract und die Funktion,
die übergeben wird, die besteht darin,
dass wir die Kleiner Gleichfunktion auf die Elemente der Liste anwenden.
Und da verwenden wir uns wieder unser Apply, um daraus
eben diese Funktion zu bauen. Und damit können wir auch
einen Vertrag schreiben, dass eine Liste aufsteigend sortiert ist.
Da würden wir Folgendes sagen, das ist eine Lamda-Ausdruck,
der sagt, diese Zahlen sind sortiert, aufsteigend.
Da verwenden wir die Sord-Funktion.
Das nächste wichtige Beispiel, das wir, Moment,
das wollte ich auslassen. Was wir besprechen wollen als Schema,
was wir auf jeden Fall kennen, müssen unbedingt,
das ist das Problem des Abbildens oder Mapping.
Funktionen höhere Ordnung, Map, das ist praktisch unser Arbeitspferd für
fast alles. Mapping ist da für die Situation da,
wo wir eine Liste von Werten haben und dieselbe Operation fließbandartig
auf jedes Element der Liste anwenden wollen.
Also stromorientierten Programmierstil würden wir es uns vorstellen,
er fließt ein endloser Strom von Werten an uns vorbei und
für jeden Wert wenden wir eine bestimmte Funktion an der Arbeitsstation,
wo wir gerade sind.
Also eine Folge oder Menge von Werten sollen in einheitlicher Weise transformiert werden,
sodass Anzahl und Anordnung der Elemente erhalten bleiben.
Und in dem Fall, wenn wir vor der Situation stehen,
dann repräsentieren wir die Menge der Werte, die wir haben als Liste.
Und dann verwenden wir die Mapping-Funktion Map,
Funktionen höherer Ordnung, die bekommt die Liste als Argument,
bekommt eine Funktion als Argument und die Funktion wird auf jedes Element der Liste angewendet.
Also hier unsere Liste von Werten, Funktionen,
die an alles auf alles angewendet werden soll und wir kriegen eben eine transformierte Liste zurück.
Die Standardfunktion Map wendet eine Funktion auf jedes Element der Liste
an und um es gleich vorweg zu sagen,
Map ist so benutzerfreundlich, dass wir beliebig viele Listen parallel übergeben können.
Und dann werden die Listen eben parallel verknüpft.
Da muss nur die Stelligkeit der Funktion, die wir übergeben,
genauso viele Argumente zulassen, wie wir Listen übergeben haben.
Also wenn wir hier Liste von Zahlen haben und wollen die alle quartrieren,
ja, potenzieren zwei hochend zum Beispiel,
dann würden wir hier schreiben, Exponent zwei hochänderts Funktionen.
Zwei Hochs, sagen wir Map, zwei hoch und übergeben hier
die Liste und dann kriegen wir die Liste, wo wir alle Zweierpotenzen der Zahlen haben.
Oder wir hatten die Liste hier mit unseren Computer,
ein Scientists, wenn nicht da unsere Funktion Last nehmen,
drüber schicke, kriegen wir eben die ganzen Nachnamen, ja,
oder wir haben mir die, schicken unsere Funktionen fört,
nehmen Tour drüber, über die Liste haben wir alle Vornamen,
Da wäre jetzt mehrere Listen parallel abbilden. Dann muss in Recket allerdings gelten,
die Listen müssen gleich lang sein, finde ich ziemlich sparsam.
Andere Programmiersprachen sind großzügige, die hören auf, wenn die erste Liste leer ist.
Die Stelligkeit der Funktion muss gleich der Zahl der Listen sein.
Also wenn wir hier eine Liste 1, 2,
3, 4, 10,
20 und 100 und so weiter haben und machen einen Map mit Plus,
dann würden eben genau an der gleichen Stelle stehenden Elemente mit Plus verknüpft.
Oder das Beispiel von eben hier, damit kriegen wir
alle Vornamen, damit kriegen wir alle Nachnamen.
Und wenn wir jetzt einen Map machen mit List,
die Resultate, die hier zurückkommen,
dann haben wir hier die echte Gleichheit alle eingeführt,
sämtliche Titel sind rausgeschmissen und wir haben jede Person nur noch mit einem Vornamen,
mit einem Nachnamen.
Und an sowas gewöhnen sich ja bitte gleich,
wir werden ziemlich geschachtelte Ausdrücke mit vielen Maps bekommen.
Das ist der Normalfall ab jetzt. Immer noch viel einfacher zu lesen,
als wenn wir das Gleiche durch eine lineare Rekursion machen müssten.
Deutlich übersichtlicher. Auch das, was wir im letzten Übungs-Off-Webbe gemacht hatten,
das Erzeugen von Assoziationslisten lässt dich sehr gut mit diesem Map machen.
Wenn wir zwei Listen haben,
dann können wir eben auf diese Weise eine Liste von Tupeln erzeugen,
von solchen Paaren, indem wir einfach sagen,
Map Cons und schicken hier zwei Listen rein und wir haben hier jetzt Tupel,
das wird jeweils das erste Element aus der ersten und das zweite Element aus der zweiten Liste kommt.
Zum Beispiel die Funktion SIP, die ich in einigen Übungsgruppen besprochen hatte.
Die könnte man genauso machen, einfach mit Map Cons
und fertig. Gut, das zweite wichtige Werkzeug,
was Sie kennen sollen, wenn es um Listen geht,
ist Filtern
Wir haben eine Liste von Werten und wollen nur bestimmte haben.
Also einfach die Aufgabe, such aus all dem,
was wir da haben, die guten Sachen raus,
guten ins Töpfchen, schlechten, guten Skröpfchen, schlecht ins Töpfchen.
Also, und dafür gibt es eben die Zahl Filter.
Wir wenden Filter an auf eine Liste und übergeben ein Prädikat,
das sagt, was wir haben wollen und was nicht.
Und damit können wir Folgendes leisten. Zum Beispiel Funktionen Filter,
die Funktion Ott übergeben, übergeben eine Liste mit Zahlen und
wir kriegen hier dann nur noch Die Unterliste aller Zahlen raus, die ungerade sind, ja?
Oder wenn wir dir typgerechten Zahlen haben wollen,
zum Beispiel filtern hier mit Peer, wir wollen alle nur die Werte haben,
die tatsächlich ein Paar sind und alles andere rausschmeißen,
ja, Filterpeer von dieser Liste hier,
da bleibt Bus übrig und zwei, drei,
ja?
Oder alle echten Wahrheitswerte raussuchen aus dieser Liste.
Dann bleiben eben nur noch Truhe und Volks übrig.
Also Map-Filter sind eigentlich alles Kurzformen für eine lineare Rekursion, ne?
Wir könnten die Filterfunktion uns so selber schreiben,
filtern mit irgendeinem Prädikat P,
die Liste von XS
Wenn die Liste leer ist, wenden wir,
fragen wir mit Nyle Access, gehen wir die leere Funktion,
der ist nichts zu filtern.
Und in allen anderen Fällen tragen wir,
ob das erste Element der Liste das übergebene Prädikat P hier erfüllt.
Und wenn es es erfüllt, dann machen wir einen Kunst.
Vom ersten Element der Liste mit dem gefilterten Rest der Liste.
Also Map ist eine solche lineare Rekursion. Filter ist so
eine lineare Rekursion. Wir können überall da,
wo wir sonst zur linearen Rekursion greifen würden,
in der Regel einfach eine dieser Standardfunktion höhere Ordnung nehmen
Was mich darauf bringt, in Zukunft die Aufgabenzettel sehr genau lesen,
wenn sie eine Aufgabe bekommen. Achten Sie darauf, ob da steht,
lösen Sie es regulsiv, weil das nochmal Rekursion üben soll?
Oder nehmen Sie eine Funktion höhere Ordnung, ja?
Gut. Wir können für Therme mit Filtern ein paar Zusicherungen betrachten.
Also wenn wir hier eine Liste haben,
die aus der Zusammensetzung zweier Liste besteht, mit Pent,
und filtern diese ganzen mit dem Prädikat, dann ist es dasselbe,
als wenn wir die erste Teilliste filtern würden,
Und die zweite Teilliste filtern würden und das Ganze mit Appent verbinden.
Also, Filter ist distributiv bezüglich der Komkartonation.
Und es gilt auch, wenn wir mehrfach filtern,
dass es unabhängig von der Reihenfolge ist, in der wir filtern.
Also erst alle Ungraden rausschmeißen und dann alle Graden rausschmeißen,
in beiden Fällen bei derselben leeren Liste.
Diese Zusicherung gelten natürlich nur dann,
wenn wir bezugstransparent programmieren
Gut, die dritte Funktion, Hörer, Ordnung,
die zum Muskel gehört,
die sie auch auf jeden Fall am Mindestsemester parat haben müssen und wissen,
wie es funktioniert, das ist das Falten.
Wenn wir an Map denken und an Filter denken,
diese beiden Funktionen bekommen eine Liste als Argument und zurückkommt eine Liste.
Filtern reduziert die Falten, kollabiert die Elemente einer Liste zu
einem einfachen Wert und das ist hinterher nicht mehr notwendigerweise eine Liste.
Zum Beispiel, wenn wir eine Liste von Zahlen haben und wollen alle agieren,
das ist ein typischer Fall für Falten,
Wir verknüpfen die Elemente der Liste, indem wir sie parweise mit Plus zusammenfassen.
Okay, also Funktion, Falten kriegt drei Argumente.
Einmal den Operator App, mit dem wir immer aufeinanderfolgende Elemente verknüpfen.
Das vorherige Resultat mit dem, was wir als nächstes Element in der Liste finden.
Wir brauchen einen Startwert, ein neutrales Element,
weil die Funktion oder der Operator, den wir übergeben,
ist zweistellig und wir gucken immer nur ein Element der Liste an und das andere Argument ist,
ein bisschen Startwert oder das, was wir als Zwischenergebnis schon haben.
Und dann übergeben wir eine oder mehrere zu faltende Listen,
je nachdem, wie viel Argumente unsere Funktionen erfasst. So,
der übergebene Operator ist, wenn wir nur eine Liste haben,
zweistellig und nimmt als erstes Argument das jeweilige Listenelement und als letztes Argument das bisherige Resultat.
Und war Beispiele dafür, wenn wir alle Zahlen in eine Liste addieren wollen,
würden wir sagen Folge, Falte,
nimm die Plusfunktion und als Staat wird null, warum null,
null ist Das neutrale Element, wenn wir die addieren wollen.
Und geht also gut, wir kriegen die sechs zurück.
Oder wir wollen alle Zahlen miteinander multiplizieren. Dann ist als
neutrales Element Null nicht so gut, wir nehmen lieber die eins.
Dann haben wir ein, zwei, drei, können wir
auch sechs zurück. Oder wir wollen mit dem Minimum falten.
Da können wir jetzt über mein neutrales Element hier streiten.
Wenn wir als Opperator das Minimum haben,
würde es sich anbieten als Startwert eine Zahl zu nehmen,
die größer ist als alles,
was eine Liste vorkommt.
Oder das erste Element der Liste
Gut. Und mal einen typischen Programmierstil zu haben,
wenn wir die Länge einer Liste ermitteln wollten, ja,
könnten wir das so machen, indem wir einfach zählen,
wie viele Elemente der Liste drin sind und das Zählen können wir machen,
indem wir die Liste auf eine Liste abbilden,
die lauter einzeln enthält.
Sozusagen, wir nehmen unsere Liste, für jedes Element,
was vorkommt, machen wir einen Strich als eine Eins.
Ja, und dann addieren wir alle Einzeln.
So,
das ist hier geschehen
Wir haben hier die Funktion X, die jedem Element eine Eins zuordnet.
Und die schicken wir über unsere Liste 1, 2,
3. Gibt uns eine Liste, die enthält 3 mal die Eins.
Und das, was da zurückkommt,
darauf wenden wir mit Fuld LD Plus-Funktionen ein neutrales Element Null.
Und wir haben hier eine neue Definition für die Längenfunktion gefunden.
Ich habe im Skript mal geguckt, also ich glaube,
ich habe in 42 Stellen unterschiedliche Varianten,
wie man funktional,
die länger einer Liste bestimmen könnte,
bringt mich
auf einen Einwand aus den Übungsgruppen
gehört habe
Also in einigen Übungsgruppen ist immer, wenn Alternativen diskutiert werden, wie mache ich das?
Kommt die Frage, welches ist denn jetzt die richtige Lösung?
Also mit dem Funktionalen programmierst du ja Gemeinde in meinem Programmierstil,
wenn sie ein Problem analysieren,
gibt es nicht einfach absolut gesehen die richtige Lösung.
Ja gut, es gibt richtige Lösungen, es gibt falsche.
Also falsche sind schon mal die, die vorgegebene Spezifikation nicht erfüllen.
Und dann gibt es richtige Lösungen und bessere. Und wichtig ist,
dass sie, wenn sie ein Aufgabenblatt überlegen, lösen wollen,
sich überlegen, welches die Anforderungen sind, die für sich erfüllen wollen.
Weil sie eine Rechenzeit effiziente Lösung haben, haben sie Vorgaben,
wie groß die Menge der eingegebenen Daten ist.
Ja, denn in manchen Fällen kann man sich leisten,
ein Verfahren zu verwenden, was quadratisch im Aufwand ist.
Bei anderen sollte man lieber ein lineares Verfahren haben und so weiter.
Also eine wirklich absolut beste,
perfekte Lösung gibt es in den meisten Fällen nicht,
wenn man eine Aufgabe hatte,
ein Problem erst analysieren muss und für sich die Randbedingungen klar macht,
bezogen auf die Randbedingungen,
die sie
sich gemacht haben
Haben Sie gute Lösungen und schlechte Lösungen? Ja.
Okay. Gut. Wir können auch das Falten noch mal
als Funktion schreiben und dann machen wir für heute Schluss.
Auch Falten ist natürlich eine lineare Rekursion über die Liste,
die wir übergeben. Erste Argument, die Funktion,
zweites der Startwert und das neutrale Element und dann die Liste,
die wir falten wollen. Und wir würden es auch so machen,
wenn die Liste leer ist, geben wir den Startwert zurück.
In allen anderen Fällen rufen wir des MyFold mit der Funktion hier wieder auf,
rufen die Funktionen auf für das erste Element und den Wert,
den wir bisher schon gesammelt haben.
Und das Ganze wenden wir auf den Rest der Liste an.
Wir haben hierfür das Falten in dieser Form eine Faltungsfunktion,
die endrekursiv ist. Das ist also eine reine Iteration über
die Liste. Sie baut kein Stack-off, ist sehr effizient.
Und es wird hier erreicht, indem die Argumente von links
beginnt in der Variablen X verknüpft werden, die als Argumulatorgeld.
Also diese Funktionen höherer Ordnung führen in Fällen wo wir Glück haben dazu,
dass wir eine reine Iteration haben und sie verwenden können,
so will sie sonst bisher in Java eben eine Vorschleife oder eine Waldschleife verwendet tippen.
Okay, damit danke für die Aufmerksamkeit
heute.
Schönes Wochenende