 Ja, herzlich willkommen zurück zur Veranstaltung Programmierparadipmen hier an der Uni Stuttgart im Sommersemester 2020. Das hier ist das vierte Modul in der Veranstaltung und es soll in diesem Modul um die Frage gehen, in welche Reihenfolge überhaupt die Instruktion in so einem Programm ausgeführt werden und genau das ist die zentrale Frage in diesem Modul zum Thema Control Flows oder auf Englisch Control Flow. Das Modul hat wieder mehrere Teile, genau genommen fünf und das hier ist der erste davon. Fangen wir mal direkt an mit einer kleinen Einführung und der Frage, was ist eigentlich Control Flows? Also es geht wie gesagt um die Frage, in welche Reihenfolge die Instruktion in einem Programm denn ausgeführt werden und das ist wahrscheinlich recht logisch in den meisten Programmiermodellen oder den meisten Modellen Berechnungen auszudrücken. Ist das natürlich eine ganz, ganz wichtige Frage denn je nachdem in welche Reihenfolge ich meine Instruktion jetzt ausführe bedeutet mein Algorithmus natürlich dieses oder jenes. In Programmiersprachen gibt es eine ganze Reihe von Mechanismen, die einem erlauben, Control Flows auszudrücken. Also ein ganz einfaches Beispiel ist das Sequencing, was im Prinzip einfach nur bedeutet, dass ich Dinge nacheinander aufzielen kann und sagen kann zuerst dieses Statement, dann jenes Statement und so weiter. Ein anderes Konstrukt, was wahrscheinlich jeder auch schon mal benutzt hat ist Selection. Also so was wie ein IF zum Beispiel, wo ich sagen kann, wenn eine bestimmte Bedingung war es, dann macht das und wenn die Bedingung falsch ist, dann macht eben was anderes. Noch ein weiteres Control Flows-Konstrukt, was wahrscheinlich auch die meisten schon benutzt haben, ist Interaktion. Also jede Art von Sprachmechanismus oder vielleicht auch API, die es einem erlaubt, Dinge mehrmals zu tun, zum Beispiel in der Schleife oder wenn ich eine Datenstruktur habe und ich über jedes Element innerhalb dieser Datenstruktur etablieren möchte. Eine andere Form ist Recosion. Da geht es auch darum, Dinge zu wiederholen, aber auf eine andere Art und Weise, nämlich indem eine Prozedur oder Funktion sie selbst wieder aufruft. Dann, und das ist ein Thema, was wir jetzt in diesem Modul nicht besprechen werden, weil es dafür dann nochmal ein Extramodul gibt, Concurrency oder Nebenläufigkeit. Nämlich der Idee, dass es nicht nur einen Control Flows zu einer Zeit gibt, sondern die Dinge eigentlich nebeneinander herlaufen und die Frage ist, wie kann man das an der Sprache ausdrücken. Das ist auch sehr spannend, machen wir dann in einem späteren Modul. Und schlussendlich gibt es in vielen Sprachen auch Exceptions, also Ausnahmen, die in bestimmten Situationen passieren und wo in bestimmten Situationen oft zum Beispiel Fehler-Situationen ein anderer Control Flows ausgeführt wird, als das vielleicht eigentlich normalerweise gedacht ist. Also wie man sieht, gibt es eine ganze Reihe von Sprachkonstrukten und jede Sprache hat dann natürlich eine andere Menge oder Untermenge von diesen Konstrukten und es gibt auch noch viele, die jetzt hier gar nicht auf der Folie draufstehen. Was entscheidend ist, ist, dass jede Sprache dann ein bisschen andere Regeln hat. Wenn man die bestimmte Sprache benutzt, muss man wie immer natürlich die Regeln dieser Sprache gut kennen. Was ich hoffe, in dieser Vorlesung rüberzubringen oder insbesondere in diesem Modul zum Thema Control Flows, ist, dass es gar nicht so sehr auf die konkrete Sündtags ankommt, die in der einen oder anderen Sprache jetzt verwendet wird, sondern dass es wichtig ist, die Konzepte dahinter zu verstehen. Also diese Idee von Iterationen und verschiedenen Arten von Schleifen und die Iteration ausdrücken können, das kommt in verschiedensten Sprachen vor. Die Sündtags variiert teilweise ziemlich stark, aber die Grundideen sind eigentlich immer dieselben und wenn man die einmal verstanden hat, dann kann man das auch relativ einfach in einer anderen Sprache dann anwenden. Bevor wir so richtig ins Thema einsteigen, würde ich gleich erstmal mit dem kleinen Quiz anfangen, und zwar mit der Intention, dass Sie einfach mal selbst über ein bestimmtes Programm diesmal in Java nachdenken und überlegen, in welcher Reihenfolge da eigentlich die Dinge ausgeführt werden und warum das so ist. Und dieses kleine Beispiel, auch wenn es sehr simpel aussieht, zeigt gleich verschiedene Dinge, die wir dann später in der Vorlesung noch ein bisschen genauer anschauen werden. Also was man hier sieht ist einfach ein kleines Stück Java und ich würde jetzt einfach jeden bitten, vor das Video gleich weiterläuft, mal auf Pause zu drücken und selbst darüber nachzudenken, was dieses Programm denn eigentlich ausgibt und dann idealerweise auch gleich im Ilias abzustimmen, um einfach zu schauen, ob das, was man sich so gedacht hat, das ist, was die anderen auch gedacht haben und schlussendlich natürlich auch, um sich selbst zu überprüfen, ob man es jetzt richtig hat. So, dann schauen wir uns mal die Lösung an. Also was hier schlussendlich rauskommt, ist der Wert 5,5. Also diese Instruktion hier oben schreibt zwei Zahlen, nämlich 5 und 5. Und jetzt ist die Frage, warum. Was wir hier in der Main Funktion hier machen, ist, dass wir diese Funktion F aufrufen und zwei Parameter übergeben. Und das erste, was man hier verstehen muss, ist, was diese Operation, die da geschehen, bevor wir die, oder vielleicht auch nachdem wir den Parameter übergeben, was die eigentlich genau bedeuten. Das erste beim ersten Parameter, dieses I++, ist einfach der PostIncrement-Operator, der, ja, gibt erst den aktuellen Wert von I zurück und inkriminiert ihn anschließend. Das heißt, was hier passiert ist, wir nehmen den aktuellen Wert von I, 5, und geben den an die Funktion F und hinterher wird das I noch inkriminiert, weil also 6 draus, aber das ist dann auch egal. Zumindest für das erste Argument. Beim zweiten Argument haben wir den Predecrement-Operator und was der macht ist, der nimmt den aktuellen Wert von I, dekriminiert ihn zuerst und gibt dann das Ergebnis davon zurück. Jetzt haben wir beim ersten Argument das I auf 6 gesetzt. Was wir hier also machen ist, wir setzen es wieder zurück auf 5, denn wir dekrimentieren, also ziehen 1 ab, geben diesen Wert 5 dann als zweites Argument in die Funktion F rein und deswegen bekommt F schlussendlich hier oben als Parameter a und b, zweimal den Wert 5 und gibt am Ende 5,5 aus. Der Grund, warum das jetzt so ist und nicht vielleicht noch ein bisschen anders, ist, dass in Java das Ganze von links nach rechts evaluiert wird. Also es steht fest, wenn wir mehrere Argumente an eine Funktion übergeben, dann werden diese einzelnen Argumente von links nach rechts evaluiert, also das linkes Sitz wird zuerst vollständig evaluiert, dann das nächste und so weiter, bis wir ganz rechts angekommen sind. Scheint recht logisch, weil man liest ja auch von links nach rechts, zumindest so in Deutsch und Englisch. Wir werden gleich noch ein paar Sprachen sehen oder eine Sprache zumindest sehen, wo das eben nicht so ist. Das heißt, dass was jetzt hier logisch erscheint, ist nicht unbedingt in jeder Sprache so. So, nach diesem kleinen Aufwärm-Quiz schauen wir uns mal an, was genau in diesem Modul jetzt eigentlich dran kommen soll. Es gibt, wie gesagt, fünf Teile. Das hier ist der erste und da geht es um die Evaluation von Expressions oder Ausdrücken. Wenn wir das dann gemacht haben, schauen wir uns an, wie man Kontrollfluss beschreiben kann in so einer Sprache, also insbesondere, wie man beschreiben kann, in welche Reihenfolgesstatements denn ausgeführt werden, und zwar einmal auf einer strukturierten Art und Weise und einmal auf einer unstrukturierten Art und Weise und was genau das bedeutet, sehen wir dann im zweiten Teil. Im dritten Teil schauen wir uns Selection ein bisschen genauer an, also die Frage, wie kann man basierend auf bestimmten Zuständen entweder diesen Kontrollfluss 2 nehmen oder jeden Kontrollfluss 2 nehmen. In Teil 4 geht es dann um Interationen, also verschiedene Arten und Weisen, wie man Schleifen und Schleifen ähnliche Konstrukte in Programmiersprachen ausdrücken kann, und schlussendlich geht es im fünften und letzten Teil dann um Rekursionen. Ja, stecken wir einfach mal direkt in diesen ersten Teil ein, und da geht es um die Evolution von Expressions oder Ausdrücken. Schauen wir erstmal an, was so ein Ausdruck oder so eine Expression denn überhaupt genau ist. Die zwei wichtigen Begriffe dabei sind Operator und Operandens. Wichtig zu wissen, was was ist, also die Operatoren sind die Funktion, die wir auf bestimmte Werte anwenden. Das sind oft so Dinge wie Plus und Minus und die Operanden sind die Werte, die wir an dieser Operation übergeben, also zum Beispiel einfach zahlen oder vielleicht die Werte, die in Variablen drinstehen oder vielleicht auch die Rückgabewerte eines Funktions aufruf ist. Hier unten sind mal drei Beispiele, also bei dem ersten gibt es genau einen Operand, nämlich das I und der Operator, der darauf angewandt wird, ist dieses Post Increment plus plus. Hier haben wir zwei Operanden, nämlich einmal den Rückgabewert von FU, was hier als erstes aufgerufen wird und dann die Zahl 23. Und der Operator, der das Ganze verbindet, ist das Plus. Und hier unten haben wir dann gleich drei Operanden, nämlich A, B und C, das sind Variablen. Und wir haben zwei Operatoren, die in der bestimmten Reihenfolge dann auch ausgeführt werden, nämlich dieser Multiplikationsoperator hier und der Divisionsoperator. Also das sind alles Beispiele für Ausdrücke, aufgeschrieben auf eine bestimmte Art und Weise und in welche Reihenfolge das dann alles ausgeführt wird und warum das so ist sehen wir gleich. Also jetzt habe ich da gerade diesen Ausdruck, diese Expression auf eine bestimmte Art und Weise aufgeschrieben. Im Allgemeinen gibt es eigentlich drei Arten von Notationen, wie man diese Expressions aufschreiben kann und die tatsächlich auch in vielen Sprachen so verwendet werden. Das erste ist die Prefix Notation, wo die Grundidee ist, dass der Operator immer am Anfang steht, gefolgt von den Operanten, auf die der Operator dann angewandt wird. Da gibt es drei Untervarianten, nämlich ohne Klammern oder mit Klammern an verschiedenen Stellen. Die Variante ohne Klammern sieht aus wie das hier. Also wir haben den Operator vorne und anschließend dann die Liste der Operanten. Dann gibt es die Variante, dass wir den Operator haben und dann in Klammern die Liste der Operanten. Wenn es nur einen Operanten gibt, dann steht da halt nur einer drin. Oder ich umklammere den kompletten Ausdruck, indem ich einfach Klammer auf dann den Operator, dann die Liste der Operanten und dann Klammer zuschreibe. Und in dem dritten Fall ist die Konvention, dass einfach das allererste, was in der Klammer kommt, immer ein Operator ist und anschließend alles, was danach dann kommt, dass er als Operanten interpretiert wird. Ein konkretes Beispiel aus einer Sprache, wo diese dritte Variante der Prefix Notation verwendet wird, das ist Lisp. Also was wir hier stehen haben ist im Prinzip 1 plus 3 und das Ergebnis davon dann mal 2. Und zwar in dieser Prefix Notation aufgeschrieben. Also der gibt einen Ausdruck hier in drinnen, nämlich das Plus von 1 und 3. Und dann gibt es noch einen Ausdruck hier außen rum, nämlich das Mal von dem, was bei dem ersten Ausdruck rauskommt und 2. Und gemeinsam ergibt das dann eben 1 plus 3 und das mal 2. Ja, das war die Prefix Notation. Die zweite Variante ist die Infix Notation, wo die Idee ist, dass der Operator in der Mitte zwischen den Operanten steht, was natürlich nur geht, wenn wir eine binäre Operation haben, also zwei Operanten haben. Kein Beispiel, wo das zumindest für viele arithmetische Ausdrücke verwendet wird ist Java. Hier ist dieselbe Expression, die wir gerade eben schon hatten, eben nochmal in Java aufgeschrieben. Also 1 plus 3 und dann das ganze mal 2. Das ist auch wieder, das sind eigentlich auch wieder zwei verschiedenen Expressions, die eine geschachtelt in die andere, plus dass wir eben jetzt die Infix Notation benutzen. Die dritte Variante ist PostFix. Was bedeutet, dass wir den Operator am Ende haben? Also wir haben erst die Liste der Operanten, vielleicht auch nur einen Operant, aber kann auch mehrere sein, gefolgt dann vom Operator. Ein Beispiel wieder, wo das Auftritt ist, zum Beispiel C, wo wir diesen PostIncrement-Operator haben, wo ich den Operanten in dem Fall das A zuerst habe und dann den Operator, der in diesem Fall das plus plus ist. Jetzt hätte ich gerade schon kurz die Multiplizität von Operatoren angesprochen, also die Frage, wie viele Operanten so einen Operator denn eigentlich erwartet oder man kann manchmal auch sagen, wie viele Argumente so einen Operator denn erwartet. Da gibt es im Prinzip unendlich viele Möglichkeiten. Praktisch kommen eigentlich nur drei vor, nämlich Unary, Binary und Ternary, also sprich ein Argument, zwei Argumente oder drei Argumente. Hier sind zwei Beispiele für Unary. Ja, Expressions und Operatoren, nämlich das plus plus, was wir gerade schon hatten oder mal einer der mit der prefix Notation häufig verwendet wird, nämlich diese Migierung. Im Falle von Binary haben wir das plus ja gerade eben schon gesehen. Eine andere Binary-Operator, den man vielleicht manchmal gar nicht als solchen wahrnimmt, ist zum Beispiel InstanceOf in Java. Das ist auch ein Operator, der einfach zwei Argumente hier nimmt, nämlich einmal das Ding, von dem wir testen wollen, ob es ein Instanz einer bestimmten Klasse ist und dieses Ding und die dazugehörige Klasse sind dann einfach die beiden operanten beziehungsweise Argumente. Ein Beispiel für einen Ternary-Operator, den es in vielen Sprachen gibt, ist dieses ja diese Kurzschreibweise für if-then-else, wo ich eine bestimmte Bedingung habe und sage, wenn die gilt, dann soll A rauskommen und falls sie nicht gilt, dann soll B rauskommen. Und das ist ja auch einfach ein eingebauter Operator dieses Fragezeichen. Doppelpunkt, was in viel Sprachen drin ist und drei Argumente nimmt. Wie gesagt, könnte man sich auch mehr ausdenken. Also man kann eine Sprache durch das Design, in der es Operatoren mit 4, 5, 6 und so viel man möchte operanten gibt, aber 1, 2 und 3 ist im Prinzip das, was praktisch wirklich vorkommt. So aus diesen operanten und operatoren kann ich mir jetzt also Ausdrücke zusammenbauen und in vielen Fällen soll so ein Ausdruck nicht nur ein Operator enthalten, sondern mehrere Operatoren. Und was ich dann bekomme, ist ein sogenannter komplexer Ausdruck, in dem ich also mehrere Ausdrücke ineinander verschachtle, sodass das Ergebnis von dem einen Ausdruck dann wieder als operant für den nächsten Ausdruck, der ringsrum geschachtelt ist, genutzt wird. Und die große Frage, und darum geht es ja auch in diese Modules, in welche Reihenfolge werden denn dann die einzelnen Operationen eigentlich ausgeführt. Mal drei Beispiele, wo das auf den ersten Blick vielleicht klar ist und vielleicht auch nicht klar ist. Das erste sind einfach mehrere arithmetische Operationen, die wir in Python haben, also wir sagen 2 plus 3 mal 4. Und die Frage ist jetzt, wird er erst 2 plus 3 gerechnet und dieses Ergebnis 5 dann mal 4 oder wird erst 3 mal 4 gerechnet und das Ergebnis davon anschließend dann mit der 2 zusammenaddiert. Könnte mal jetzt so eine Intuition haben, schon aus der Schule, so Punktrechnung geht vor Strichrechnung und so, aber wie das in der Programmiersprache definiert ist, könnte theoretisch auch anders sein und wie das genau definiert ist, sehen wir dann gleich. Ein anderes Beispiel ist hier diese Wildemix aus Boolean Expressions und anderen Expressions in Java. Also wir haben da einmal diesen Operator, der das X negiert, dann haben wir hier dieses logische Und und dann haben wir da noch so einen Gleichheitsvergleich und auch hier ist wieder die Frage, in welche Reihenfolge wird das Ganze denn jetzt evaluiert oder ausgefühlt? Also ich könnte zum Beispiel erst mal das hier evaluieren und zum Beispiel schauen, ob das X und das A logisch war der Falsche ergeben, dann könnte ich das zum Beispiel negieren und anschließend mit diesem Forzie hinten vergleichen, ob das so richtig ist oder nicht hängt von den Regeln der Sprache ab und wir sehen gleich wie genau diese Regeln eigentlich festgelegt werden. Drittes Beispiel kommt diesmal aus C und zwar habe ich hier zwei Operatoren, einmal dieses Post Increment mit dem plus plus und das andere das De-referenzieren eines Pointers mit dem Sternchen und auch hier wieder ist die Frage wird erst der Pointer incrementiert und schriegt sozusagen die Adresse hinter dem aktuellen Pointer und den Wert der da steht oder passiert das Ganze genau andersrum und auch hier hängt es wieder davon ab, wie genau das nun in der Sprache definiert ist. Jetzt haben wir diese ganzen verschiedenen Möglichkeiten gesehen, in der Operation innerhalb von einem komplexen Ausdruck ausgeführt werden könnten und die Frage ist jetzt, wie sieht das dann in der echten Sprache aus? Also woher weiß man in der echten Sprache, in welche Reihenfolge die Operation jetzt ausgeführt werden und die Antwort ist, dass es in jeder Programmiersprache zwei Arten von Regeln gibt, die eben genau das festlegen und das sind die Präzidents und die Assoziativitätsregeln. Also die erste Gruppe von Regeln, hier diese Präzidentsregeln oder auf Englisch Precedents, legen fest, welche Operatoren quasi Präzidents haben, also enger zusammenbinden und wenn ich jetzt einen komplexen Ausdruck habe, in dem zwei Operatoren drin sind, dann wird zuerst das ausgerechnet, was eben von dem Operator zusammengebunden wird, der die höhere Präzidenz hat und anschließend dann die andere Operation. Die zweite Art Regel sind die Assoziativitätsregeln oder Assoziativität auf Englisch. Hier geht es um Operatoren, die gleiche Präzidenz haben und wo man dann vielleicht nicht, also rein aus die Präzidentsregeln nicht wüsste, in welche Reihenfolge der komplexe Ausdruck jetzt evaluiert werden soll und was diese Assoziativitätsregeln dann machen, ist festzulegen, ob der Ausdruck nach links oder nach rechts geklammert wird, also ob zuerst das auf der linken Seite oder zuerst das auf der rechten Seite ausgerechnet wird. Ja, schauen wir uns erstmal die Präzidentsregeln ein bisschen genauer an und zwar am Beispiel von C. Also wie gesagt jede Sprache, jede Programmiersprache hat ihre eigenen Präzidenzregeln, was wir hier sehen ist Teile der Regeln, die es für C gibt. Das bedeutet nicht, dass es in anderen Sprachen dann auch so sein muss. Praktisch ist es so, dass C ja schon immer einen großen Einfluss hatte und auch noch hat auf Sprachen, die später entwickelt wurden und deswegen ist vieles von dem, was wir hier sehen, tatsächlich auch in anderen Sprachen so, aber das muss nicht so sein, weil jede Programmiersprache kann das wirklich anders festlegen. Was wir hier sehen, ist also so eine Tabelle und jede Zeile in dieser Tabelle zeigt uns an, welche Operatoren die selbe Präzidenz haben, also auf dem selben Präzidentslevel sind. Alles, was weiter oben steht, hat höhere Präzidenz als alles, was weiter unten steht. Das heißt, also wenn ich zum Beispiel einen komplexen Ausdruck habe, in dem sowohl dieser Post-Increment-Operator hier drüben vorkommt als auch, sagen wir mal, eine Pointer-D-Referenz, dann wird zuerst der Post-Increment-Operator ausgeführt und dann die Pointer-D-Referenz. Also die Reihenfolge der Tabelle gibt an, in welche Reihenfolge die Operationen dann ausgeführt werden. Nachdem, die ich gerade schon gesagt habe, kommen dann so verschiedene Vergleiche. Zuerst die Inequalities, also größer Gleich und kleiner Gleich, dann die Equality Vergleiche, wo auch das Nicht-Gleichsein mit reinzählt. Anschließend haben wir logische Operatoren. Zuerst das Und und dann das Oder und ganz unten in dieser Liste kommt ein Assignment. Das heißt, wenn ich ein Assignment habe, wird im Prinzip erst alles andere ausgerechnet und dann passiert schlussendlich das Assignment. Wichtig ist, wie gesagt, dass diese Liste nicht vollständig ist. Also im Ziel gibt es natürlich noch andere Operatoren und wer jetzt für einen anderen Operator gerne wissen möchte, wie der sich zu denen, die hier aufgelistet sind, verhält, muss einfach in der Sprachspezifikation nachschauen und da ist dann genau aufgeschlüsselt, in welche Reihenfolge die Operatoren ausgeführt werden und zwar eben mit Hilfe dieser Präzidenz regeln. So mit Hilfe dieser Präzidenz regeln und speziell der Tabelle, die ich gerade gezeigt habe, können wir uns jetzt zum Beispiel so komplexe Ausdrücke in 10 mal anschauen und darüber nachdenken, was bedeuten die eigentlich. Also hier ist eine, die wir vorhin schon hatten, dieses D-referenzieren plus, nee, D-referenzieren P++ und die Frage ist, wird erst der Pointer inkrementiert oder wird erst, ja, der Pointer dereferenziert und dann der Werte dahinter steht, inkrementiert und was hier passiert ist, weil der Post-Increment-Operator eine höhere Präzidenz hat als der D-referenzierung-Operator, bedeutet, dass das im Prinzip so geklammert würde, wenn man Klammern drum schreiben würde, nämlich, dass zuerst das P++ ausgerechnet wird und anschließend dann das Ergebnis davon dereferenziert wird. Das heißt, ich schaue mir die Adresse an, die in P steht, inkrementiere die und dereferenziere die dann, also lese dann den Wert, der an dieser Adresse tatsächlich steht. Hier im zweiten Beispiel sind einfach verschiedene logische Operatoren miteinander vermischt, nämlich einmal das logische und und dann das logische oder. In der Tabelle hatten wir gesehen, dass das logische und höhere Präzidenz hat und das spiegelt sich dann einfach dadurch wieder, dass dieses A und B zuerst evaluiert wird und das Ergebnis davon dann mit dem logischen oder mit C verknüpft wird. Hier unten noch ein Beispiel, wo verschiedene Vergleiche oder Gleichheitsoperatoren verknüpft sind. Also hier schauen wir x kleiner y gleich gleich fuh und weil die Ungleichheitsoperatoren, also hier dieses kleiner gleich, ne kleiner ohne gleich, von x und y höhere Präzidenz hat, weil da oben steht in der Tabelle wird zuerst dieser Vergleich gemacht und dann das Ergebnis davon mit dem Fuh verglichen. Also diese Präzidenzregeln helfen ganz einfach, das auseinander zu halten und und so dass man im Prinzip die Klammer nicht hinschreiben muss. Die allgemeine Regel, die ich raten würde, unabhängig davon, was die Programmiersprache jetzt festliegt, ist, dass wenn man sich nicht sicher ist oder vielleicht nicht sicher ist, ob derjenige, der den guck mal lesen möchte, sich sicher ist, dann am besten immer doch Klammern verwenden. Also die Sprache legt eigentlich fest, in welche Reihenfolge die Teilausdrücke evaluiert werden. Aber natürlich möchte man den Code auch so einfach wie möglich lesbar halten und in vielen Fällen bedeutet das, dass man am besten doch die Klammern drum macht. Die Präzidenzregeln der Sprache helfen uns jetzt also rauszufinden, in welche Reihenfolge so ein komplexer Ausdruck evaluiert wird, wenn ich darin Operatoren aus verschiedenen Präzidenzleveln habe. Die Frage ist jetzt, was passiert, wenn ich in einem komplexen Ausdruck mehrere Operatoren habe, die aber auf demselben Präzidenzlevel sind oder vielleicht ganz einfach mehrmals denselben Operator habe. Und hier kommt die zweite Gruppe von Regeln zum Einsatz, die es auch in praktisch jeder Sprache mit komplexen Ausdrücken geben muss, nämlich die Assoziativitätsregeln. Die sagen uns hier, ob so ein Operator von links nach rechts dann berechnet wird oder von rechts nach links oder anders ausgedrückt, ob das dann links assoziativ oder rechts assoziativ ist. Wir werden es nicht im Detail besprechen, sondern ich gebe einfach mal zwei Beispielen für zwei Gruppen von Operatoren, die es in praktisch jeder Sprache gibt und zwar zum ersten die arithmetischen Operatoren. Was hier gilt ist, dass in den meisten Sprachen die meisten arithmetischen Operatoren von links nach rechts berechnet werden. Das heißt, diese Operatoren sind links assoziativ. Zum Beispiel das Minus. Wenn ich jetzt in der Programmiersprache sowas hinschreibe wie hier, 12 minus 3 minus 2, dann heißt das in den meisten Sprachen, dass wir von links nach rechts rechnen. Das heißt, dieses Minus hier vorne hat eine stärkere Bindung als dieses Minus hier hinten. Das heißt, wir rechnen zuerst 12 minus 3 aus, macht 9 und dann das Ergebnis die 9 minus die 2, sodass wir dann auf 7 kommen. So läuft in den meisten Sprachen, wenn Minus jetzt rechts assoziativ wäre, wäre es eben genau anders herum. Das heißt, wir würden zuerst 3 minus 2 ausrechnen und das Ergebnis dann von 12 abziehen, also hätten wir 12 minus 1 und da kommt eben was anderes raus als die 7. Wie gesagt, in den meisten Sprachen und mit den meisten arithmetischen Operatoren geht es von links nach rechts. Eine Ausnahme ist die Potenzierung oder auf Englisch Exponentiation, denn diese in den meisten Sprachen eben rechts assoziativ, also wird von rechts nach links berechnet. Wenn ich also zum Beispiel sowas hinschreibe wie hier, 2 hoch 3 hoch 2, dann wird zuerst dieser Potenzoperator, Potenzierungsoperator auf der rechten Seite gebunden. Das heißt, ich rechne erst 3 hoch 2 aus und rechne dann 2 hoch, das Ergebnis darf dann, also in dem Fall 2 hoch 9 und komme in den meisten Sprachen dann auf 512. Eine Ausnahme von dieser Regel ist zum Beispiel Excel, wo es ja auch arithmetische Ausdrücke in diesen Formeln gibt und da kann ich auch 2 hoch 3 hoch 2 hinschreiben und hier geht es aber von links nach rechts. Das heißt, es wird zuerst 2 hoch 3 gerechnet und das Ergebnis 8 dann noch mal hoch 2 und deswegen komme ich da dann auf 64. Also hier auch wieder wichtig, man muss die Regeln der Sprache kennen oder wenn man sich nicht sicher ist, einfach klammern sitzen. Die zweite Gruppe von Operatoren, die wir hier ein bisschen genauer anschauen wollen, bezüglich Assoziativität, sind Assignments und hier ist quasi die Frage, ob wir zuerst das Assignment auf der linken Seite oder zuerst das Assignment auf der rechten Seite ausführen und hier geht es an den meisten Sprachen, wir von rechts nach links vorgehen, also Assignments sind meistens rechts assoziativ. Wenn ich also sowas hinschreibe wie a ist gleich b ist gleich a plus c, dann bedeutet das, dass wir das Ergebnis von a plus c in das b reinschreiben, weil eben hier dieses erste ist gleich stärker bindet und dann das Ergebnis davon auch noch mal in das a reinschreiben, weil wir dann das zweite ist gleich ausführen. So, als kleinen Test, ob das ganze jetzt halbwegs hängen geblieben ist, haben wir wieder ein kleines Quiz und zwar geht es da, ja sind eigentlich zwei kleine Quiz, nämlich einmal um diese Aufgabe hier oben, wo wir ein kleines Programm haben, in dem es zwei integer Variablen gibt und dann steht da so ein Ausdruck, in dem zwei Assignments drinstehen und die Frage ist, was sind, nachdem das ganze ausgeführt wurde, denn die Werte von fu und ba für den Fall, dass Assignments links assoziativ sind oder eben, dass sie rechts assoziativ sind. Das zweite ist dann hier unten, wo die Frage ist, was ist denn der Wert von z, nachdem all das, was hier steht ausgeführt wurde, für den Fall, dass das logische und höhere Präzedenz hat, als das logische oder, oder eben, dass es genau andersrum ist und das logische oder höhere Präzedenz hat, als das logische und. Drücken Sie jetzt mal Pause und denken drüber nach und stimmen dann bitte in Ilias ab, um einfach wieder zu überprüfen, ob sie es richtig gemacht haben und für mich auch so ein bisschen als Feedback, um zu sehen, was denn die Leute so hier alles verstanden haben. Ja, schauen wir mal die Lösung an und zwar fangen wir mit dem ersten an. Also fu ist am Anfang 1 und base am Anfang 2. Wenn Assignments jetzt links assoziativ wären, das heißt wir würden zuerst dieses Assignment ausführen, dann würde fu den Wert von ba kriegen, also fu Wert 2 und anschließend würden wir dann dieses Assignment hier ausführen. Das heißt, wir würden dann fu plus ba rechnen, also 2 plus 2, gleich 4 und das nach ba schreiben. Also hätten wir schlussendlich das fu gleich 2 ist und ba gleich 4. Wenn Assignments jetzt rechts assoziativ wären, dann säßt anders aus, denn dann würden wir zuerst dieses Assignment ausführen, das heißt fu plus ba ausrechnen, das ergibt 3, schreiben das dann nach ba, deswegen ist ba dann hier 3 und anschließend fühlen wir das zweite Assignment aus und haben dann fu auch noch mit dem Wert 3. Schauen wir mal das zweite Beispiel an, dass die Frage was ist der Wert von z. Wir haben ja diese verschiedenen bullshit Variablen x ist false, y ist false, z ist true. Wenn das logische und jetzt höhere Präzidenz hat, als das logische oder also so wie es zum Beispiel in c ist, würden wir zunächst dieses y und y hier in der Mitte ausrechnen, false und false ist false und dann hätten wir das x, also false oder false oder z und das ist true und kann damit insgesamt auf true. Wenn es jetzt andersrum wäre, also wenn das oder höhere Präzidenz hätte als das logische und, würden wir zunächst diese beiden Ausdrücke hier ausrechnen, also x oder y, false oder false ist false und dann y oder z, das ist false oder true und da gibt es also true und dann haben wir false und true und da da nicht beide true sind, ist das insgesamt dann also false. Also was diese Beispiele hier zeigen ist, dass die Präzidenzregeln und die Assoziativitätsregeln natürlich eine Rolle spielen, denn je nachdem wie genau die in der Sprache definiert sind, kommt eben das eine raus oder das andere. So, jetzt haben wir schon ganz viel darüber geredet, in welche Reihenfolge denn die Operatoren ausgeführt werden, also die Operation, was wir noch nicht diskutiert haben, ist in welche Reihenfolge die Operanden denn überhaupt evaluiert werden. Schauen wir uns einmal konkret das Beispiel an, um überhaupt zu sehen, was da das Problem ist. Also wenn wir so was hinschreiben wie a minus f von b minus c mal d, dann geben uns die Regeln, die wir bisher hatten natürlich schon einiges vor, denn wir wissen zum Beispiel, dass die Multiplikation höhere Präzidenz hat als die Subtraktion, deswegen wird zuerst dieses c mal d ausgerechnet und was wir jetzt auch gesehen haben ist, dass zumindest in den meisten Sprachen die Subtraktion links assoziativ ist, das heißt wir wissen, dass dieser Teilastdruck hier auf der linken Seite vor dem zweiten minus evaluiert wird. Was wir bis jetzt noch nicht wissen, ist in welcher Reihenfolge denn die Operanden evaluiert werden, also werden wir zuerst f von b ausrechnen oder werden wir zuerst c mal d ausrechnen. Jetzt können wir fragen, ja ist das dann überhaupt wichtig, warum spielt das eigentlich eine Rolle und da gibt es zwei Gründe, der erste Grund sind Side Effects oder Seiteneffekte, jetzt weiß hier das Problem, dass das Evaluieren eines Ausdrucks natürlich auch Daten wieder modifizieren kann, die dann den Einfluss auf spätere Ausdrücke haben, die anschließend evaluiert werden. Für unser konkretes Beispiel, was wir hier unten hatten, war hier die Frage, ob wir f von b zuerst evaluieren oder zuerst c von d. Jetzt kann es ja sein, dass, sagen wir mal, c eine globale Variable ist und dass f von b, also diese Funktionsaufruf, diese globale Variable schreibt und dann spielt es natürlich eine Rolle, ob wir zuerst f von b evaluieren oder zuerst c mal d. Der zweite Grund sind, dass der Compiler bestimmte Optimierung nur dann ausführen kann, wenn das eben den Regeln der Sprache entspricht und quasi die Garantie, die die Sprache bietet, bezüglich der Reihenfolge, in der Dinge ausgeführt werden, dann auch einhält und je nach dem, ob der eine Teil aus Druck jetzt vielleicht vor dem anderen oder in unserem Fall der eine operant vor dem anderen evaluiert wird, kann der Compiler dann bestimmte Optimierung wie zum Beispiel Registerallokation oder auch die Frage, in welche Reihenfolge die Instruktion denn tatsächlich auf der CPU gescheduled werden ausführen. Also das ist nicht nur für das Verständnis der Programmierer wichtig, sondern eben auch für die Implementierung der Sprache. So die Antwort auf die Frage, in welche Reihenfolge die verschiedenen Operanten jetzt evaluiert werden, hängt wie so oft von der konkreten Programmiersprache ab. Also manche Sprachen definieren das so und andere Sprachen definieren das wieder anders und manche, wie man gleich sehen werden, definieren es auch gar nicht. Also in vielen Sprachen, zum Beispiel Java oder C-Sharp gilt, dass grundsätzlich von links nach rechts evaluiert wird. Das heißt wir würden für unser Beispiel zuerst den Funktionsaufruf von F evaluieren und dann das C mal D. In einer Sprache nämlich C ist das Ganze nicht definiert und nicht definiert heißt in dem Fall, dass der Compiler entscheiden kann, wie er davor gehen möchte. Das heißt er kann von links nach rechts evaluieren, er kann aber auch von rechts nach links evaluieren. Das ist in dem Sinne gut für den Compiler, weil er dann nämlich das auswählen kann, was die besten Optimierung ermöglicht und so schlussendlich eventuell zu schnellerem Code führt. Aus Programmierersicht ist es vielleicht nicht ganz so ideal, weil man dann eben nicht weiß, was schlussendlich passiert. Und um das Ganze mal zu illustrieren, schauen wir einfach nochmal dieses Beispiel an, was wir vorhin so als kleine Aufwärmübung ja schon hatten, wo diese Funktion F aufgerufen wurde und zwar mit zwei Argumenten i plus plus und minus minus i. Vorhin hat man gesehen, dass in Java hier fünf und fünf übergeben wird, weil ja die plus plus und minus Operatoren so definiert sind, wie sie nochmal definiert sind und das Ganze eben von links nach rechts evaluiert wird. Im Falle von C kann interessanterweise sowohl 5,5, also die links nach rechts Variante übergeben werden, als auch 4,4. Denn wenn jetzt nämlich das Ganze von rechts nach links evaluiert wird, passiert Folgendes. Wir haben zuerst den Wert i, der wird zunächst dekrementiert, das heißt auf 4 gesetzen, das Ergebnis davon dann als zweites Argument übergeben. Anschließend haben wir hier dieses i plus plus, wo der aktuelle Wert von i, also 4, gelesen wird, zurückgegeben wird, also dann als erstes Argument ein F übergeben wird und anschließend wird i wieder inkrementiert. Das heißt i ist am Ende nach dem dieser Aufruf stattgefunden hat, wieder 5. Aber das, was wir an die Funktion f übergeben, ist eben 4 und 4. Ganz interessantes Beispiel, wer da irgendwo mal drüber stolpert, wenn er C schreibt, sollte dann also jetzt nicht mehr so überrascht sein, weil man jetzt weiß, warum das tatsächlich so passieren kann. Interessanterweise, weil das eben in der Sprache nicht definiert ist, kann Folgendes passieren, nämlich dass jemand solchen Code hinschreibt, das Ganze mit einem Compiler für eine Maschine 5,5 übergibt und dann woanders kompiliert oder für eine antrisch Maschine kompiliert, plötzlich 4,4 übergibt, was natürlich sehr überraschend sein kann. Ein anderer interessanter Spezialfall, was die Evaluierung von komplexen Ausdrücken angeht, und das ist jetzt etwas, was wir in den meisten Programmiersprachen so haben, ist die sogenannte Short Circuit Evaluation. Hier ist die Grundidee, dass wir, wenn ein Bullschirr Ausdruck evaluiert wird, eventuell Zeitsparen können, indem wir gar nicht alles evaluieren müssen, was nötig ist, sondern eigentlich dann aufhören können, wenn wir das Ergebnis von diesem Bullschirr Ausdruck, also true oder false, wissen. Mal als konkretes Beispiel, wenn ich hier so ein Logisches und in dem if drinstehen habe, so was wie if very unlikely and very expensive, dann muss dieser zweite Teil Ausdruck, nämlich dieser Funktionsaufruf von very expensive, gar nicht unbedingt ausgeführt werden, sondern er muss nur ausgeführt werden, wenn der erste Teil Ausdruck dieses very unlikely, dann tatsächlich auch zu true evaluiert. Denn wenn ich schon weiß, dass der erste Teil Ausdruck zu false evaluiert, dann weiß ich, dass das logische und mit, was auch immer dahinter kommt, auf jeden Fall auch zu false evaluieren wird. Das heißt, es gibt eigentlich keinen Grund, dieses very expensive auch zu evaluieren. Ein Seiteneffekt, den das Ganze haben kann, ist, dass die Seiteneffekte, die die Evaluation des zweiten Teilesdruckes eigentlich hätte, dann unter Umständen nicht stattfinden. Das heißt, je nachdem, ob der erste Teil Ausdruck dieses very unlikely zu true oder false evaluiert, wird diese Funktion very expensive aufgerufen oder eben auch nicht. Das heißt, sämtliche Seiteneffekte, zum Beispiel das Schreiben von anderen Variablen, die ich in dieser Funktion very expensive drin habe, können, müssen aber nicht ausgeführt werden. Die meisten Programmiersprache nutzen diese Short Circuit Evaluation, wie ich gerade schon gesagt habe, also insbesondere eben für zwei Fälle, zum einen für das Bulge und wo der zweite Operant einfach nicht evaluiert wird, wenn der erste E schon zu false evaluiert wurde und dann außerdem auch noch fühlt das Bulge oder das Logische oder wo der zweite Operant ignoriert wird, sofern wir schon gesehen haben, dass der erste zu true evaluiert, weil auch dann wissen wir ja schon, was das Ergebnis des Gesamtausdruck sein wird, nämlich einfach true. Eine Ausnahme, doch eine relativ populären Sprache, die zumindest früher mal sehr populär war, ist Pascal, denn hier gibt es keine Short Circuit Evaluation, das heißt, der Code, den wir auf der Folie vorher gesehen haben, hätte da plötzlich eine ganz andere Bedeutung, denn da steht immer fest, dass sowohl der erste als auch der zweite Teil aus Druck evaluiert wird. Das heißt, man muss also in allen Sprachen, wo es diese Short Circuit Evaluation gibt, darauf achten und sich bewusst sein, dass in so einem Bulgen Ausdruck die zweite Hälfte unter Umständen eben nicht ausgeführt wird. Man kann das natürlich aber auch zu seinem Vorteil benutzen und es ist sogar so ein Programmieridium, was in vielen Sprachen durchaus gängig ist. Also hier ist mal ein kleines Beispiel aus C, wo wir durch eine Liste iterieren und einfach immer schauen, ob es in der Liste tatsächlich noch ein nächstes Element gibt. Das heißt, wie haben wir irgendwo diesen pointer P, der uns ein Element dieser Liste gibt und schauen dann im Header dieser Wildschleife an, ob dieses P denn überhaupt noch definiert ist oder dass das nicht Null ist. Und wenn das der Fall ist, dann wissen wir, dann wird der zweite Teil aus Druck evaluiert. Und was wir dann machen ist, dass wir anschauen, ob der Wert in dem Element dieser Liste gespeichert ist, irgendeinem anderen Wert gleich oder ungleich ist. Das Wande ist, dass wir diesen zweiten Teil aus Druck aber eben nur dann ausführen, wenn das hier vorne nicht Null ist, also wenn das zu True evaluiert. Denn wenn das P hier vorne schon zu False evaluieren würde, weil zum Beispiel P Null ist, dann würden wir das hinten auch nicht machen und wenn wir es täten, würden wir dann Null die referenzieren, was natürlich nicht funktionieren würde, sondern zum Absturz des Programms führen würde. Ja, damit sind wir auch schon am Ende dieses ersten Teils des Moduls Control Flow oder Control Fluss. Sie haben jetzt also gesehen, in welcher Reihenfolge die Teilausdrücke eines komplexen Ausdruckes evaluiert werden und welche Arten von Regeln es in Programmiersprachen da überhaupt gibt, insbesondere Präzidenz und Associationsregeln, die festlegen, in welche Reihenfolge das alles passiert. Damit danke fürs Zuhören und bis zum nächsten Mal.