 Ja, herzlich willkommen zurück zu Programmierparadigmen und zu diesem zweiten Teil des vierten Moduls zum Thema Kontrollfluss. Worum soll es hier gehen? Das Thema dieses zweiten Teiles ist Strukturierte und Unstrukturierte Kontrollfluss. Ganz allgemein gesagt, was das bedeutet ist, Strukturierte Kontrollfluss ist alles, wo uns die Sündtags der Sprache und die Struktur, die sich daraus im Programm ergibt eigentlich sagt, in welche Reihenfolge die Dinge ausgeführt werden. Also ein Beispiel ist eine Vorschleife, wo ich weiß, am Ende der Schleife geht es wieder zur Bedingung der Schleife und anschließend da halt wieder in den schleifen Body oder dann eben auch hinter die Schleife. Aber dieser Struktur, die von der Sprache selbst vorgegeben wird, sagt mir, welche Möglichkeiten der Reihenfolge, in der das Ganze ausgeführt werden kann, gibt es denn? Und im Gegensatz dazu hat man bei unstrukturierten Kontrollfluss das eben nicht, sondern man hat die Möglichkeit an mehr oder weniger beliebige Stellen im Programm zu springen. Und im Allgemeinen sind sich die Programmiersprachendesigner oder auch Software-Ingenieure einig, dass Strukturierte Kontrollfluss besser ist, weil man ganz einfach einfacher sehen kann, in welche Reihenfolge die Instruktion des Programms schlussendlich ausgeführt werden. Das wahrscheinlich bekannteste Beispiel für unstrukturierten Kontrollfluss sind die sogenannten Go-Tos. Historisch kommen die aus Assemblersprachen, wo wir ja schon vor vielen, vielen Jahrzehnten die Möglichkeit gehabt haben, unter einer Bedingung oder vielleicht auch ohne eine Bedingung an irgendeine andere Stelle im Programm zu springen. Das sind die sogenannten Conditional- oder Unconditional-Jumps, wo ich einfach sage, wenn diese Bedingung wahr ist, dann springe an diese oder jede Adresse im Programm oder teilweise eben auch ohne eine bestimmte Bedingung zu haben, wo man einfach sagt, sobald du hierher kommst, springe dahin. Und genauso wie bei den Go-Tos, die es in Programmiersprachen teilweise gab oder auch noch gibt, gibt es bei diesen Jumps keine Bestimmung, wo man denn eigentlich genau hinspringen kann, sondern man kann im Prinzip überall hinspringen. In früheren Programmiersprachen, die teilweise natürlich auch noch heute verwendet werden, gibt es das dann in der einen oder anderen Form auch, nämlich mithilfe dieses Go-To-Statements, wo man irgendwo im Programm sogenannte Statement-Labels angeben kann, also bestimmter Label, die einfach sagen, hier ist ein Statement, das hat jetzt einen Namen. Und diese Labels dienen dann als Ziel für das Go-To und ich kann irgendwo im Code dann sagen, springe jetzt zu diesem Statement, dass diesen Namen hat und egal wo ich dann bin und welchem Konstrukt der Sprache ich vielleicht gerade drin bin, anschließend springt der Kontrollfluss eben genau zu diesem gelabelten Statement und dann geht es eben da weiter. Vielleicht mal zur Veranschaulichung ein konkretes Beispiel und zwar aus einer der Sprachen, die heute doch noch häufig verwendet werden, wo die Go-Tos tatsächlich noch drin sind, nämlich C. Also hier haben wir ein Stück Code, was irgendwo so ein Go-To Statement enthält und irgendwo auch so ein Statement-Label hat, zu dem man dann hinspringen kann. Was der Code macht ist, wir haben eine Variable A und dann diese Do-Wild-Schleife, die solange iteriert bis A oder solange A kleiner als 15 ist und am Ende dieses Schleifen-Bodies wird A inkrementiert. Was wir außerdem machen ist, dass wir den aktuellen Wert von A einfach als Dezimalwert ausgeben. Das heißt, wenn jetzt das hier oben nicht noch stünde, würde der Code 10, 11, 12, 13, 14 ausgeben. Was jetzt aber hier oben steht, ist, dass wir in einem bestimmten Fall, nämlich wenn A gleich 12 ist, dass A auch inkrementieren, so weit, so gleich wie vorher, aber dann anschließend direkt zu diesem My-Label springen, also quasi von hier direkt wieder nach hier oben gehen. Schöner Fall. Und das bedeutet, dass wir dann insbesondere das, was hier unten steht, also auch dieses Print-Statement für den Fall dann überspringen. Das heißt, was der Output dieses Programms schlussendlich ist, ist eben nicht 10, 11, 12, 13, 14, sondern 10, 11, 13, 14, weil wir für den Fall, dass A 12 ist, die zwei Statements hier unten nicht ausführen, sondern eben direkt wieder zum Beginn der Schleife springen. Das Beispiel von gerade eben war ja noch mehr oder weniger verständlich. Um jetzt mal die nicht so schönen Seiten von GoTo zu beleuchten, habe ich hier ein etwas komplexeres Beispiel. Also es sind auch nicht besonders viele Code-Zeilen. Was wir hier aber haben, ist jetzt eben nicht nur ein GoTo und auch nicht nur ein Label, sondern wir haben drei davon, nämlich hier und hier, drei Label und auch drei GoTos, die dann jeweils zu diesen Labeln gehen. Und die Frage, die ich an Sie jetzt als kleines Quiz stellen möchte, dass Sie dann bitte auch in Ilias beantworten ist, was macht dieser Code eigentlich oder insbesondere, was gibt er denn hier unten aus? Also am besten an der Stelle des Videos, dann einfach mal pausieren wieder, nachdenken, was der Code macht und dann bitte in Ilias abstimmen, um zu checken, ob man das Ganze auch verstanden hat und auch zu sehen, was die anderen so abgestimmt haben. Ja, schauen wir mal die Lösung an. Unter den Auswahlmöglichkeiten, die es da gab, ist die richtige Lösung something else, also nämlich keine der angegebenen anderen Lösungen, denn dieses Programm terminiert leider niemals, sondern läuft ewig und erreicht niemals dieses Print Statement hier am Ende. Der Grund ist folgender, also wir haben da diese zwei Variablen, Resalt und Number, gehen zunächst erstmal hier in diese Vorschleife rein hier oben und rechnen zu Resalt den aktuellen Wert von i, also null hinzuspringen dann aber von hier direkt hier zu diesem Label 2, wo wir dann schauen, ob das Resalt kleiner als 2 ist. Das ist der Fall und deswegen springen wir dann zu 3, wo wir anschließend wieder i, das ist leider immer noch null, zu Resalt hinzu addieren, das heißt Resalt bleibt weiter null und dann springen wir wieder hier runter, schauen ob Resalt größer als 2 ist, ist immer noch nicht der Fall, gehen wieder hoch zu Label 3 und so weiter. Und so geht es quasi endlos im Kreis und der Grund, warum wir dann hier rauskommen ist, weil wir eben diesen unstrukturierten Kontrollfluss haben und deswegen auch nie zu diesem Schleifenheder kommen, indem wir das i inkrementieren und deswegen hängen wir in der endlos Schleife fest. Die Tatsache, dass diese Gotus und der unstrukturierte Kontrollfluss, den man damit so ausdrücken kann, nicht besonders elegant ist und vermutlich zu mehr Fehlern führt als alternative Arten, wie man Kontrollfluss ausdrücken kann. Das hat die Informatik zum Glück schon relativ zeitig festgestellt und insbesondere kam das in einem Artikel von Ezgar Dijkstra zum Ausdruck der 1968 mit dem Titel Gotus Statement Considered Harmful erschienen ist. Und was der Artikel im Prinzip macht, ist Beispiele geben und argumentieren, warum Gotus schlecht sind und warum das eigentlich eine schlechte Praxis ist und dass man doch stattdessen strukturierte Kontrollflusskonstrukte benutzen sollte. Insbesondere sollte man Algorithmen eben ausdrücken in dem Sequencing, also einfach das nacheinanderstellen von Statements, Selection, also sowas wie If und Iteration, also verschiedene Schleifenkonstrukte benutzt werden, weil das eigentlich ausreichend ist, um beliebige Algorithmen, die man in Programmiersprachen ausdrücken kann, auszudrücken, ohne dass man Gotus braucht. Um zu zeigen, warum das vor einigen Jahrzehnten nicht so ganz unkontrovers war und für die, die Gotus auch heute noch lieben, was man vielleicht stattdessen benutzen sollte, ist hier mal eine kleine Liste von Fällen, wo Gotus früher häufig benutzt wurden und wo man sie vielleicht auch heute noch benutzen könnte und dann auf der rechten Seite die strukturierte Kontrollflussalternative dazu. Ein Beispiel, wo Gotus doch öfters verwendet worden ist, um an das Ende einer bestimmten Subproutine zu springen, um dann sozusagen aus der Subproutine oder Funktion wieder rauszukommen und stattdessen gibt es in praktisch ein Programmiersprachen heutzutage Return Statements, mit denen ich im Prinzip genau das erreiche, also ich kann einfach an das Ende der Funktion springen und von der Funktion wieder zurückkehren. Ein zweites Szenario, wo Gotus öfters mal zum Einsatz kam, ist, dass ich in der Mitte einer Schleife bin und aus irgendeinem Grund jetzt aus der Schleife aber raus möchte und die restlichen Iteration eben nicht mehr ausgeführt werden sollen und genau das kann ich in den meisten modernen Sprachen mit einem Break Statement machen und außerdem kann ich zum Beispiel auch nur die aktuelle Iteration der Schleife oder das Ende davon überspringen mit dem Continuous Statement. Drittes Beispiel ist, dass ich manchmal aus meinem aktuellen Kontext zum Beispiel meine aktuelle Funktion gern rausmöchte und und zwar sofort und auch dafür wurden Gotus häufig früher eingesetzt. In modernen Programmiersprachen gibt es aber schon dessen Exceptions, die mir eben erlauben, aus einem beliebigen Kontext heraus sofort rauszukommen und bis zu einer bestimmten Stelle, wo diese Exceptions dann behandelt wird zu springen. Also all das sind die all diese Varianten oder all diese Kontrollflusskonstrukte, die wir hier auf der rechten Seite sehen, sind im Prinzip eingeführt worden, um Alternativen für Gotus zu bieten und somit die Mächtigkeit von diesem Gotus zumindest zum Teil abzubilden, ohne dass man aber all die schlechten Nebenwirkungen, nämlich das komplett unstrukturierte Irgendwo hinspringen, damit bekommt, so dass der Coach los endlich einfacher zu schreiben und vor allem auch einfacher zu verstehen ist. Jetzt habe ich gerade argumentiert, warum man Gotus möglichst nicht verwenden sollte. Wird jetzt aber trotzdem ein Konstrukt vorstellen, was die Gotus nochmal vereigemeinert, also im Prinzip noch mächtiger ist als die Gotus, die wir gerade eben schon gesehen haben. Und dieses Konstrukt nennt sich Continuation. Diese Continuations sind ein sehr, sehr mächtiges Sprachfeater, denn insbesondere erlauben sie dem Programmierer bestimmte Kontrollflusskonstrukte nachzubilden, die vielleicht in der bestimmten Sprache gar nicht drin sind. Also sowas wie Exceptions oder Iteraton oder auch Co-Routine, die wir später noch kennenlernen werden, kann man alles mit Continuations nachbilden. Das heißt, obwohl ich jetzt nicht empfehlen würde, mit Gotus oder Continuations die ganze Zeit zu programmieren, ist es ganz gut. Und zumindest diese Continuations manchmal in der Sprache zu haben, weil man damit dann Dinge ausdrücken kann, die sonst vielleicht nicht ausdrückbar werden oder die sonst nur sehr kompliziert aufzuschreiben werden. Was ist jetzt also so eine Continuation? Also die Grundidee ist, dass ich den Kontext, in dem ich mich gerade befinde, in irgendein Form abspeichern kann, so dass ich dann später genau aus diesem Kontext heraus weiter ausführen kann. Ein bisschen genauer beschrieben besteht so eine Continuation aus drei Teilen. Nämlich zum einen eine Code-Adresse, die mir sagt, da geht es also weiter. Das ist ein bisschen wie das Label von einem Gotu. Also ich kann wirklich genau sagen, an welchem Statement oder welche Instruktion es anschließend weitergehen soll. Das zweite ist ein Referencing-Environment, was wir ja im letzten Modul schon kennengelernt haben. Was uns also sagt, welche Werte an welchen Namen gebunden sind, so dass wenn ich dann von dieser Code-Adresse die Ausführung fortsetze, ich tatsächlich auch weiß, welche Bedeutung Namen, die in diesem Programm existieren, denn eigentlich haben. Und schlussendlich gibt es noch eine zweite Continuation, die uns nämlich sagt, wo wir weiter machen sollen, wenn der Code, zu dem ich dann gesprungen bin, returned, also ein Return-Statement aufruft, so dass ich weiß, wo es da danach dann auch wieder weitergehen würde. Am besten schauen wir uns ganz immer mit dem konkreten Beispiel an, und zwar in einer Programmiersprache, die recht populär ist und gleichzeitig Continuations für den Programmierer anbietet, nämlich Ruby. Was wir hier sehen, ist einmal eine ganz normale Funktion in Ruby. Und dann hier unten dieser Aufruf von Code CC, den werden wir uns gleich genauer anschauen, und schlussendlich dann eine Aufruf von Print F, der einfach eine Dezimalzahl, die in V gespeichert ist, ausgeht. So fangen wir mal hier unten an. Also was hier zunächst passiert ist, dass wir dieses Code CC aufrufen. Und was das macht, ist, dass es eine Continuation erstellt. Das heißt, Code CC gibt oder erstellt ein Objekt, mithilfe dessen wir dann zu einem beliebigen Zeitpunkt wieder an genau diese Stelle im Programm, an dem dieses Code CC aufgerufen wurde, zurückspringen können. Was dieses D hier ist, ist, dass das eine Referenz auf eben diese Continuation ist. Also das D ist ein Objekt, mithilfe dessen wir dann an diese Stelle wieder zurückspringen können. Und was wir hier jetzt in dem Beispiel machen, ist, dass wir dieses Objekt nehmen und FU aufrufen. Und zwar mit dem Wert 1 als ersten Argument und eben dieser Continuation oder diese Referenz auf die Continuation als zweites Argument. Das heißt, wir kommen schlussendlich hier oben bei diesem Aufruf von FU raus. Das erste Argument des I wird 1 sein und das, was hier oben dann C heißt, zeigt eben auf die Continuation, die ich erstellt habe. Zunächst erstmal gibt FU dann was aus und schreibt den aktuellen Wert von I. Das heißt, am Anfang schreiben wir 1, dann schauen wir, ob I kleiner als 3 ist und solange das der Fall ist, rufen wir FU wieder auf. Und zwar mit I plus 1, also am Anfang dann zum Beispiel mit 1 plus 1, also gleich 2 und derselben Continuation, die wir hier einfach wieder nach FU reingeben. Das heißt, FU ruft sich mithilfe dieses rekursiven Aufrufs noch 2 mal auf, nämlich zum Anfang ja FU von 1, dann FU mit 2 und dann FU mit 3. Und 3 ist dann nicht mehr kleiner als 3, das heißt, wir gehen dann in den Else Branch und das heißt, nachdem es dann zum dritten Mal aufgerufen wurde, rufen wir jetzt, nutzen wir schlussendlich diese Continuation, also dieses Continuation.call bedeutet einfach, dass wir jetzt an die Stelle zurückkehren, wo die Continuation erstellt worden und das übergeben dieses Wertes I hier führt schlussendlich dazu, dass dieses call CC aus Sicht des Kodes hier unten als Rückgabewert eben genau diesen Wert, den wir hier an call übergeben, also das I hat. Das heißt, schlussendlich sieht es so aus, als würde call CC 3 zurückgeben und das ist dann, was wir schlussendlich auch in der Ausgabe sehen, denn wir geben 3 mal diesen Beginn der Methode FU aus und anschließend springen wir aber direkt zu dem Print F hier unten und geben die 3 aus und interessanterweise kommen wir nie zu diesem Print F hier, obwohl wir ja eigentlich diese FU Methode ausgeführt haben, aber wir springen so aus der Mitte der Ausführung raus, direkt wieder hierhin und geben deswegen dieses end hier niemals aus, sondern dann direkt dieses God 3. Schauen wir uns zu verdeutlichen nochmal ein zweites Beispiel an, das ist auch wieder in Ruby und hier sehen wir auch wieder diese Verwendung dieser call CC API, die eben eine Continuation erstellt und schlussendlich wird die dann hier unten aufgerufen, indem wir eben wieder dieses Objekt nehmen, indem die Continuation drin ist und dann Punkt Call aufrufen und in dem Fall die Continuation selbst nochmal als Argument übergeben. Um zu verdeutlichen, was hier passiert, mal ich mal noch den Call Stack des Programms auf, also der geht irgendwo los mit dem Main-Programm, was in dem Fall einfach nur das Hauptscript ist, also sozusagen der ganze Code, der dann hier unten ist. Und was in diesem Main-Programm zunächst passiert, das Folgendes, dass wir end gleich 3 setzen, also end ist 3 und anschließend dann bar aufrufen. Wenn wir bar aufrufen, macht bar Folgendes, es gibt erstmal aus, dass es aufgerufen wurde und gibt uns auch den aktuellen Wert von I mit aus, das heißt beim ersten Aufruf von bar, der dann so auf dem Call Stack erscheint, ist I gleich 1 und was ausgegeben wird an der Stelle, ist start und dann 1. Anschließend überprüft bar, ob I kleiner als 3 ist, da I gleich 1, es ist das der Fall und in dem Fall ruft sie es nochmal auf mit I plus 1 als Argument, das heißt wir haben hier einen weiteren Aufruf von bar, wo I jetzt gleich 2 ist und dementsprechend wird dann auch dieses Print F Statement wieder aufgerufen und wir schreiben noch start 2 und weil 2 immer noch kleiner als 3 ist, gibt es noch einen Aufruf von bar, nämlich hier oben, das kommt dann wieder auf den Call Stack drauf und I ist in dem Aufruf dann 3 und dementsprechend wird dann auch start 3 ausgegeben. Was jetzt bei diesem dritten Aufruf von bar passiert, ist, dass wir wieder überprüfen, ob I kleiner als 3 ist, I ist aber gleich 3 und deshalb gehen wir diesmal nicht in den Van Brunch, sondern hier in den Else Brunch und was hier passiert, ist, dass wir dieses hier haben und das ist einfach ein Funktionsaufruf. In Ruby kann man die Klammern hinter dem Funktionsaufruf weglassen und genau das ist, was hier passiert ist, aber das ist schlussendlich nichts anderes als ein ganz normaler regulärer Aufruf, der uns dann eben hier hoch zu hier führt. Das heißt auf unserem Call Stack haben wir dann noch ein Activation Record, nämlich den für den Aufruf von hier und was in hier nun passiert, ist, dass wir Call CC aufrufen, also eine Continuation erstellen, die eben genau den Zustand in diesem Moment, inklusive der Kotstelle, in der wir uns befinden, abspeichert. Das heißt die Continuation, wenn wir uns das in dem Stack so anschauen wollen, ist dann genau hier oben. So jetzt ist die Frage, was passiert dann mit dieser Continuation? Also Call CC gibt die zurück, die hier Funktionen gibt, dieses Objekt, was die Continuation repräsentiert, als Rückerbewert zurück. Das heißt der Aufruf von hier schreibt die dann schlussendlich hier in diese Variable B und bevor die verwendet wird, haben wir noch dieses Print, was jetzt End ausgibt und den aktuellen Wert von i und der ist gerade 3 und dann wird diese Continuation hier zurückgegeben. Das heißt wir auf unserem Call Stack ist dieses hier schon wieder weg. Jetzt kehren wir auch noch von dieser dritten Aufruf aus bar zurück, sind also wieder im zweiten Aufruf von bar. An der Stelle kehren wir dann also von diesem Aufruf hier zurück und geben auch noch mal End aus und zwar mit dem aktuellen Wert den i in diesem zweiten Aufruf von bar hat und das ist nämlich 2. Kehren dann auch wieder von bar zurück. Falsche Farbe sind im ersten Aufruf von bar. Da passiert wieder das Gleiche. Wir geben auch noch mal End aus mit dem aktuellen Wert von i und das ist jetzt 2 und damit sind wir aus allen Bars wieder raus und sind wieder in unsere Main Funktionen. Das heißt wir sind also genau hier an dieser Stelle jetzt hinter dem Aufruf von bar. End wird jetzt von 3 auf 2 runtergesetzt, also 3 minus 1 und was hier relevant ist, auch noch ist der Wert den c jetzt hier hat, also weil die Continuation in B gespeichert war hier oben und wir das B immer schön weiter zurückgegeben haben, ist schlussendlich, dass c hier unten auch ein Name für diese Continuation und die ist schlussendlich im Main Programm unter diesem Namen c zur Verfügung. Was wir dann hier machen, ist, dass wir schauen ob End größer gleich 0 ist. End hatten wir hier auf 2 gesetzt, das heißt es ist nicht größer als 0 und was wir nun machen ist dann die Continuation zu nutzen, indem wir sie aufrufen und dieser Aufruf führt uns schlussendlich wieder genau dahin, wo die Continuation erstellt wurde, nämlich hier oben hin, kurz vor die Stelle, bevor wir aus hier zurück kommen und das bedeutet im Prinzip, dass unser Call Stack plötzlich wieder so aussieht, wie ihr gerade eben schon aussah, also dass wir diese ganzen Elemente, die eigentlich schon weg waren, weil wir davon ja returned waren, plötzlich alle wieder sehen und wir sozusagen an der Stelle nochmal weitermachen. Genau, hier steht es auch nochmal, also weil End in dem Moment 2 ist, springen wir wieder an die Continuation und siehe da, dann sind wir eben wieder genau an der Stelle, wo wir vorhin schon waren, wo wir nämlich aus hier dann zurückkommen und das führt dazu, dass wir dieses Zurückkommen von hier jetzt nochmal ausführen. Also wir sind wieder an der Stelle, wo wir End ausgeben, und zwar mit dem aktuellen Wert, den wir jetzt haben, also wir sind jetzt wieder hier. Die hier Funktion ist zurückgekehrt, wir nehmen den aktuellen Wert von i und der ist jetzt wieder 3 und dann passiert das Ganze nochmal und wir kennen wieder zurück und schreiben nochmal End 2 und das Ganze nochmal und wir schreiben nochmal End 1, dann ist auch diese Aufruf von bar weg und wir sind wieder im Main Programm und das Ganze passiert jetzt nochmal, weil jetzt wird End von 2 auf 1 gesetzt, das heißt, weil End jetzt 1 ist und 1 ja immer noch größer als 0 ist, springen wir nochmal zur Continuation, das heißt, es geht quasi nochmal so ein bisschen zurück in der Zeit und der Stack ist plötzlich wieder so da, wie er schon vorher da war, der passiert das Gleiche nochmal, ich schreife es jetzt mal ein bisschen schneller hin, also wir haben noch 3 Aufrufe von bar, die jeweils wieder zurückkommen, das ist dann wieder alles weg und dann setzen wir schlussendlich End auf End minus 1, 1 minus 1 ist 0, das heißt, 0 ist nicht mehr größer 0 und an der Stelle sind wir dann tatsächlich fertig, weil wir jetzt eben nicht mehr in dieses Ding hier reingehen und nicht nochmal Call CC aufrufen, sorry, nicht nochmal Call aufrufen auf der Continuation, sondern stattdessen dann dieses Dann ausgeben. Ja, das heißt also, was der Kot hier schlussendlich ausgibt, ist eben genau das, was hier nochmal steht. Start 1, Start 2, Start 3, dann End 3, End 2, End 1 für das erste Mal zurückkehren und dann eben noch 2 Mal dieses End 3, 2, 1, weil wir 2 Mal wieder zu dieser Continuation gesprungen sind und erst dann, weil End dann eben gleich 0 ist, schlussendlich das dann. Ja, das soll es auch schon wieder gewesen sein zum Thema Strukturierter und Unstrukturierter Kontrollfluss, Sie haben jetzt gesehen, was Goutous sind, gehört, dass man die eigentlich nicht benutzen sollte und dann dieses doch etwas verrückte Konstrukt von Continuations kennengelernt, mit dem man unter anderem Goutous und auch andere Sachen modellieren kann. Wie gesagt, die Goutous sollte man auf jeden Fall nicht verwenden, die Continuations sollte man auch nicht unbedingt so in seiner Standard Programmierertool-Kiste haben, aber die sind manchmal ganz praktisch, um eben bestimmte Konstrukte zu emulieren, die vielleicht in der Programmiersprache sonst eigentlich gar nicht drin sind. Und damit vielen Dank fürs Zuhören und bis zum nächsten Mal.