 Ja, herzlich willkommen zu Programmierparadigmen. Wir sind hier im zweiten Themenblock, nämlich dem Themenblock Syntax und in der zweiten Einheit dieses Themenblocks, in dem es um kontextfreie Grammatiken gehen wird. Wir hatten uns ja angefangen, damit zu beschäftigen, wie man spezifizieren kann, was überhaupt Teil einer Programmiersprache ist, also welche Zeichenketten gehören dazu und welche gehören nicht dazu. Und was wir im letzten Einheit gesehen haben, ist, dass reguläre Ausdrücke benutzt werden können, um die Tokens einer Programmiersprache zu spezifizieren. Und die Frage, mit der wir uns jetzt beschäftigen wollen, ist, wie kann man diese Tokens jetzt zu kompletten Programmen zusammensetzen und wie kann man spezifizieren, welche Arten dieses Zusammensetzens denn überhaupt erlaubt sind und welche nicht erlaubt sind. Das Ganze werden wir tun, indem wir uns kontextfreie Grammatiken anschauen und als kleine Motivation, warum wir die überhaupt brauchen, fangen wir mal an mit der Frage, warum reguläre Ausdrücke eigentlich nicht genug sind. Sind die vielleicht genug? Ich wechsle mal zu den Notizen und die Frage ist also, sind reguläre Ausdrücke vielleicht genug? Und warum sind sie nicht genug? Also warum beschäftigen wir uns hier noch mit kontextfreien Grammatiken? Als Beispiel, um das ein bisschen zu motivieren, gehen wir mal davon aus, dass wir eine Sprache haben, in der wir arithmetische Ausdrücke haben. Das ist in den meisten tatsächlichen Programmiersprachen ja der Fall. Also ich kann arithmetische Ausdrücke wie 5 plus 7 oder sowas in den meisten Sprachen ja aufschreiben. Also was wir zum Beispiel gern dabei hätten, wäre eben genau dieser Ausdruck 5 plus 7. Wir hätten aber zum Beispiel auch gern den Ausdruck 5 plus 7, einmal Klammern drum und das Ganze nochmal plus 6. Oder wir hätten gern auch den Ausdruck 5 plus 7 mit Klammern drum und plus 6 und dann nochmal Klammern um das Ganze minus 23. Und was man hier sieht ist, dass in den regulären oder in den arithmetischen Ausdrücken, die wir hier haben, tatsächlich noch andere arithmetische Ausdrücke drin sind. Also die Ausdrücke können innenlander verschachtelt werden. Also wir haben diesen einen Ausdruck hier oben und der taucht dann auch nochmal hier unten drin auf. Aber dieses ganze Ding ist auch wieder ein arithmetischer Ausdruck und hier das gleiche, wir haben den von der Zeile oben drüber und das Ganze ist jetzt aber auch wieder ein arithmetischer Ausdruck. Das heißt innerhalb des einen syntaktischen Konstruktes haben wir dieselbe als syntaktisches Konstrukt nochmal und das bedeutet im Prinzip, dass wir Verschachtelung brauchen. Also jeder dieser Kästen ist erstmal ein arithmetischer Ausdruck und um das Ganze jetzt ausdrücken zu können, brauchen wir irgendein Mechanismus, mit dem wir Dinge verschachteln können und mit dem wir diese syntaktischen Konstrukte innenlande verschachteln können. Und das ist eben genau das vierte Konzept zum Ausdrucken von Syntax, was ich am Anfang in der letzten Einheit schon mal kurz erwähnt hatte, nämlich die Rekursierung. Und woher kriegen wir jetzt die Rekursion? Ja, das ist genau das, was uns die kontextfreien Kramatiken erwarten, weil informal ausgedrückt sind kontextfreie Kramatiken, dasselbe wie reguläre Ausdrücke plus noch Rekursionen dazu. Als Beispiel habe ich hier mal die kontextfreie Kramatik für die arithmetischen Ausdrücke, die wir gerade eben an Beispiel schon gesehen haben, aufgeschrieben. So eine Kramatik sieht so aus, dass wir wieder so ähnliche Regeln haben mit dem Pfeil in der Mitte, wie auch schon bei regulären Ausdrücken, plus dass wir jetzt auf der linken Seite sogenannte Nicht-Terminale haben, also Symbole, die schlussendlich zu anderen Sachen abgeleitet werden können. Und zu was steht auf der rechten Seite, wo wir Sequenzen von Nicht-Terminalen und Terminalen haben, die uns beschreiben, wodurch diese Nicht-Terminale auf der linken Seite ersetzt werden können. Die Terminale in dieser kontextfreien Kramatiken entsprechen dann den Tokens der Sprache, also wir haben irgendwo durch reguläre Ausdrücke beschrieben, wie ein Identifier oder eine Number und so weiter aussieht und benutzen das dann, um schlussendlich zu beschreiben, wie arithmetische Ausdrücke in unserer Sprache erstellt werden können. Der entscheidende Unterschied zu den regulären Ausdrücken, die wir vorher hatten, ist, dass wir hier nun eben Rekursionen haben, also dass der Ausdruck Expression eben nicht nur auf der linken Seite, sondern gleichzeitig auch auf der rechten Seite erscheint und wir definieren, was eine Expression ist, indem wir uns wieder auf diese Expression beziehen. Um das ein bisschen genauer zu definieren, schreibe mir erstmal formal auf, was denn überhaupt so eine kontextfreie Kramatik ist und zwar gebe ich da die formale Definition, die Sie so vielleicht auch schon mal in der theoretischen Informatik gesehen haben, aber ich möchte einfach sicherstellen, dass die jeder kennt und dann auch in dem Kontext die Sachen, die wir hier besprechen, darauf basierend versteht. Also das wird die Definition für eine kontextfreie Kramatik. So eine Kramatik besteht aus vier denen, die ich mit N, T, R und dem kleinen S bezeichne und zwar ist N eine endliche Menge von Nicht-Terminalen. Also in unserem Beispiel von gerade eben war das zum Beispiel dieses Nicht-Terminal für Expression oder für OP. Dann haben wir T, das ist auch wieder eine endliche Menge, allerdings diesmal die endliche Menge der Terminale und diese Terminale sind genau das, was normalerweise dann die Tokens der Programmiersprache sind, die man durch reguläre Ausdrücke ausdrücken kann. In der Sprachtheorie nennt man das das Alphabet der Sprache und im Kontext von Programmiersprachen bedeutet das, dass es eben die Tokens der Sprache sind. Dann haben wir hier noch das große R, das ist auch wieder eine endliche Menge oder genau gesagt eine endliche Relation, die Nicht-Terminale auf Ketten von Nicht-Terminal und Terminalen abbildet und das ist im Prinzip die formale Art und Weise aufzuschreiben, dass wir diese Produktionsregeln haben. Also die bildet ein N ab auf eine Verkettung von Nicht-Terminalen und Terminalen und zwar null oder mehr von denen zusammen gekettet und das entspricht eben genau diesen Produktionsregeln. Und dann haben wir schlussendlich noch das kleine S, das ist ein Start-Symbol, das ist ein Nicht-Terminal, bei dem wir anfangen Strings, die von unserer Grammatik erkannte werden sollen, abzuleiten. Diese Definition reicht jetzt im Prinzip aus, um kontextfreie Grammatiken aufzuschreiben. Wenn man das praktisch machen möchte, ist das noch ein bisschen umständlich, weil da einfach ein paar Sachen fehlen, mit denen man sich relativ ausführlich das Aufschreiben von Grammatiken ersparen kann. Und ich möchte einfach diese Erweiterung jetzt auch noch kurz vorstellen. Das Wichtige daran ist zu verstehen, dass die Erweiterung dieser Definition eigentlich keine prinzipiell neuen Dinge hinzufügen, sondern eigentlich nur Abkürzungen sind, um die Grammatik ein bisschen kürzer aufschreiben zu können. Das eine ist wieder der Clean Star, den wir ja schon bei den regulären Ausdrücken kennengelernt haben, weil ein Beispiel, wo das vielleicht bei einer Programmiersprache relevant sein könnte, wenn ich zum Beispiel aufschreiben will, dass es irgendwo eine Liste von Identifier gibt, also eine Liste von Namen, die der Programmierer wählen kann, dann kann ich das mithilfe des Clean Stars so aufschreiben, dass ich sage, da ist ein Identifier gefolgt von Komma und noch einem Identifier. Und dieses Komma und noch ein Identifier kann beliebig oft wiederholt werden, also genauso wie bei den regulären Ausdrücken, heißt das, dass das null oder mehr mal wiederholt werden kann. Und um zu beweisen, dass das jetzt tatsächlich nichts Fundamental Neues ist, sondern eigentlich nur eine Abkürzung, schreibe ich mal, wie man das auch noch etwas ausführlicher hinschreiben könnte, weil das ist nämlich einfach nur die Abkürzung für zwei Regeln, die so aussehen würden, dass ich sage, eine ID-List besteht entweder aus einer ID oder, und dann füge ich, um dieses oder auszudrücken, einfach noch eine zweite Regel hinzu, wo ich sage, es könnte auch eine ID-List sein, gefolgt von einem Komma und einer ID. Ähnlich zum Clean Star gibt es auch noch das Clean Plus, was im Prinzip genau dieselbe Idee implementiert, plus, dass es jetzt nicht null oder mehr Wiederholungen sind, sondern mindestens eine, also eine oder mehr Wiederholungen. Und die dritte syntaktische Erweiterung, die wir hier noch vornehmen, ist, dass wir anstatt mehrere Regeln aufzuschreiben, die uns sagen, dass es das sein kann oder das oder das, ähnlich zu den regulären Ausdrücken, auch wieder diesen vertikalen Strich verwenden. Und zum Beispiel haben wir den gerade schon gesehen in der Regel für die, für die Operatoren, wo wir sagen können, dass das ein Plus sein kann oder ein Minus oder der Multiplikationsoperator oder der Geteiltoperator. Und wieder als Beweis, dass das eigentlich nichts Neues ist, ich könnte stattdessen auch einfach vier Regeln untereinander schreiben, die genau das Gleiche ausdrücken. Das sieht dann so aus, ich hätte eine Regel für Plus, eine Regel für Minus und so weiter, bis ich alle vier beschrieben habe. So, jetzt haben wir aufgeschrieben, wie so eine Grammatik überhaupt aussieht. Die Frage ist jetzt, was können wir eigentlich damit machen? Und das, was man üblicherweise damit macht, ist, dass man Ableitungen macht, also dass man zeigt, ob ein gegebener String tatsächlich legal ist in dieser Grammatik oder dass man die Grammatik benutzt, um legale Strings zu generieren. Und für diese Ableitung ist der Algorithmus eigentlich ganz einfach. Wir fangen beim Startsymbol der Grammatik an und ersetzen dann nicht Terminale, die da noch drin sind, so lange bis nur noch Terminale übrig sind oder bis wir vielleicht festgestellt haben, dass es nicht weitergeht. Dann haben wir gezeigt, dass der String tatsächlich nicht in dieser Grammatik oder von der Sprache, die die Grammatik beschreibt, ein Teil ist. Das heißt, der Algorithmus funktioniert so, wir haben das Startsymbol und wiederholen dann so lange bis kein Nicht-Terminal übrig bleibt, dass wir jeweils ein Nicht-Terminal, was noch existiert, auswählen und eine Produktionsregel auswählen, die eben genau dieses Nicht-Terminal auf der linken Seite hat, dann dieses Nicht-Terminal durch das, was auf der rechten Seite der Produktionsregel ist, ersetzen. Und falls es mal mehrere Optionen geben sollte, weil wir zum Beispiel diesen vertikalen Strich verwenden, dann müssten wir auswählen, welche der Möglichkeiten des Ersetzens wir tatsächlich benutzen wollen. Schauen wir uns das ganze mal an einem Beispiel an, und zwar nehmen wir wieder das Beispiel der arithmetischen Ausdrücke, was wir jetzt ja gerade eben schon verwendet haben. Ich schreibe noch mal kurz die Grammatik hin und jetzt, wo wir die Grammatik auch formal definiert haben, kann ich da auch die richtigen Begriffe für nehmen. Also wir haben dieses Nicht-Terminal-Expression, was bedeutet, dass das ein Identifier ist oder eine Number oder eine andere Expression oder eine Expression mit einer Klammer drum oder eine Expression gefolgt von einem Operator und einer Expression, und die Operatoren können entweder plus sein oder minus oder Multiplikation oder geteilt. Und wenn ich jetzt einen bestimmten String gegeben habe, dann kann ich für den die Ableitung aufschreiben, zum Beispiel machen wir das mal für fu mal x plus bar. Die Frage ist jetzt, ist das ein legaler String entsprechend unserer Grammatik? Und um das rauszufinden, müssen wir das ganze ableiten, und da fangen wir an mit dem Startsymbol, also mit Expression. Und dann benutze ich immer diesen Doppelfall, der bedeutet, dass ich eine Sache zu einer anderen ableite. So, in dem ersten Fall habe ich jetzt nur einen Nicht-Terminal, das heißt ich muss dieses Nicht-Terminal auswählen und ersetze es durch etwas, was ich auf der rechten Seite der entsprechenden Produktionsregel sehe. Und da würde ich zum Beispiel einfach mal die letzte Option nehmen. Ich ersetze also Expression durch Expression op Expression. Jetzt habe ich da drei Nicht-Terminale, die ich weiter ableiten könnte. Ich entscheide mich einfach mal für das letzte und ersetze das durch etwas, was die Produktionsregeln erlauben, zum Beispiel durch ein Identifier. Jetzt habe ich noch zwei übrig, die ich weiter ersetzen könnte, nämlich Expression und op. Ich entscheide mich wieder für das ganze Rechts, nämlich op und ersetze op durch den Operator plus. Das ist ein Terminal und damit sind wir an der Stelle mit den hinteren beiden auch schon fertig und haben nur noch diese vordere Expression übrig, die ich jetzt wieder ersetzen kann durch irgendwas, was in der ersten Regel auf der rechten Seite steht. Und in dem Fall nehme ich mal wieder die letzte Option und ersetze es durch Expression op Expression und habe dann natürlich noch mein plus und Identifier. Jetzt wieder das selbe Spiel. Ich suche mir die Nicht-Terminale, die noch drin sind und ersetze sie Schritt für Schritt mithilfe der Produktionsregeln, zum Beispiel dieses Expression durch Identifier, anschließend op durch den Multiplikationsoperator. Und dann bleibt noch diese eine Expression übrig, die ich schlussendlich auch noch ersetze durch noch mal Identifier und habe dann schlussendlich id mal id plus id. Und das, was jetzt noch übrig ist, sind sozusagen nur noch Tokens. Und was diese Tokens genau beinhalten können, ist ja durch einen regulären Ausdrucksetberat beschrieben und in dem Fall entspricht der erste Token dem Fuh, der mittlere Token dem X und der letzte Token dem Bar. Das heißt, wir haben gezeigt, dass der String, den wir hier oben hatten, tatsächlich legal ist entsprechend unserer kontextfreien Grammatik. Das, was wir hier kriegen, ist natürlich erstmal diese Ableitung in dieser Schritt für Schritt Form, wie ich sie aufgeschrieben habe. Was man auch machen kann, ist, dass man die Ableitung in Form eines Baumes darstellt. Und zwar nennt sich das dann Pastry. Die Idee von diesem Baum ist, dass die Wurzel ganz oben das Startsymbol enthält und ich dann für jede Anwendung einer Produktionsregel einen weiteren Knoten einfüge und schlussendlich bei den Leaf-Noten rauskomme, die eben dann den Tokens entspricht. Schauen wir uns das Ganze mal für das Beispiel an, was wir gerade schon gesehen haben. Also für dieses Beispiel male ich jetzt einfach mal noch den Baum hin und Sie werden dann gleich die Gelegenheit haben, das Ganze mal für ein ähnliches Beispiel selbst zu machen. Also wir hatten hier oben die Expression als Wurzel, die wurde abgeleitet nach Expression-Op-Expression. Die hintere Expression hatten wir abgeleitet nach Identifier und das entsprach dem Bar. Dieses Op hatten wir abgeleitet nach Plus und die Expression hier vorne wurde nochmal abgeleitet nach Expression-Op-Expression. Die hintere Expression ist wiederum wieder Identifier, der zu X gehört. Dieser Operator hier war der Multiplikationsoperator und die vordere Expression ist der Foo Identifier. Und somit haben wir einfach den Pastry, der eben genau zu diesem Beispiel passt. Also wenn man sich jetzt die Grammatik anschaut, kann man sich vielleicht fragen, ob es nicht vielleicht auch noch eine andere Grammatik gebe, mit der wir unsere aromatischen Ausdrücke beschreiben könnten. Und im Allgemeinen ist es so, dass es für jede Sprache, insbesondere für jede Programmiersprache, eben nicht nur eine Grammatik gibt, sondern es gibt unendlich viele Grammatiken. Der Grund, warum es unendlich viele gibt, ist, dass man im Prinzip immer weitere Nichtterminale einfügen könnte, die man eigentlich gar nicht braucht und somit die Grammatik größer und anders machen kann als alle anderen Grammatiken, die es vielleicht schon gibt. Und trotzdem beschreibt man immer noch dieselbe Sprache, weil die Nichtterminale dann einfach wieder auf sie selber zeigen. Interessanterweise gibt es Grammatiken, die besser oder schlechter geeignet sind, um eine Programmiersprache zu beschreiben. Und insbesondere gibt es da zwei Eigenschaften, die man eigentlich gerne hätte. Das eine ist, dass die Grammatik eindeutig sein sollte. Was bedeutet, dass es für jeden String, also für jedes Programm, genau eine mögliche Ableitung gibt und eben nicht mehrere mögliche Ableitungen geben könnte. Wenn man so eine eindeutige, also unambiguous Grammar hat, dann wird dadurch das Paar sind einfacher, weil nämlich in dem Moment, wo das Programm gepasst werden muss, damit beschäftigen wir uns in den kommenden Einheiten noch genauer, muss der Paser nicht verschiedene Wege ausprobieren, diesen String abzuleiten, weil es gibt ja nur ein. Das heißt, man kann in kürzere Zeit das Programm tatsächlich pasen. Die zweite Eigenschaft, die man eigentlich gerne hätte für eine Grammatik zum Beschreiben einer Programmiersprache, ist, dass die Struktur der Grammatik die internen Eigenschaften der Programmiersprache widerspiegelt. Also für arithmetische Ausdrücke zum Beispiel möchten wir vielleicht in irgendeiner Form schon kodieren, dass bestimmte Operatoren ein höheres Gewicht haben als andere Operatoren. Also, dass man Punktrechnungen, Vorstrichrechnungen zum Beispiel. Und genau das kann man ausdrücken, indem man die Grammatik so aufbaut, dass die Struktur dem, was semantisch dahinter steckt, dann tatsächlich schon entspricht. Für das Beispiel, das wir gerade schon gesehen haben, habe ich hier mal eine bessere Version dieser Grammatik für arithmetische Ausdrücke. Das beschreibt genau dieselbe Sprache, wie die Grammatik, die wir nun schon angeschaut haben, aber eben auf eine bessere Art und Weise, nämlich ist diese Grammatik eindeutig. Das heißt, es gibt pro String tatsächlich nur einen Weg, die Ableitung dafür aufzuschreiben und die Grammatik spiegelt außerdem die Struktur von arithmetischen Ausdrücken, so wie sie dann auch weiter verarbeitet werden in dem Compiler wieder. Die Grammatik beschreibt also, dass es für jede Expression, dass eine Expression entweder aus einem Term besteht oder aus einer Expression einem additiven Operator und einem Term. Ein Term kann ein Faktor sein oder ein Term kombiniert mit einem multiplikativen Operator und einem Faktor. Dann schreiben wir auf, was ein Faktor ist, was dann wieder rekursiv auf die Expressions verweist und schlussendlich haben wir unten in den letzten beiden Regeln definiert, was denn diese additiven und multiplikativen Operatoren überhaupt sind. Als kleine Übung für Sie, um das Ganze jetzt mal zu testen, würde ich Sie bitten, diese neue Grammatik, die wir gerade definiert haben, zu nutzen und damit denselben String, den wir vorher schon hatten, also fuh mal x plus bar, mal zu pausen, so auf Papier und entsprechend den Pastry dafür aufzuzeichnen. Wenn Sie das richtig gemacht haben, dann zählen Sie bitte mal die Anzahl der Kanten und Knoten und dann gehen Sie ins Ilias und stimmen ab, wie viele Kanten und Knoten Sie denken in diesem Pastry denn zu finden. Und das ist eigentlich eine ganz gute Art und Weise, um mal zu überprüfen, ob die, ja das, was wir hier in den letzten Minuten besprochen haben, denn tatsächlich auch angekommen ist. Also ich drück mal hier auf meinen berühmten Knopf und Sie drücken dann bitte mal Pause und nehme mein Zettel raus und schreibe das mal auf, einfach um das wirklich mal zu durchdenken und nicht einfach nur immer nur alles vorgerechnet zu bekommen. So, dann zeige ich mal die Lösung dazu, das ist nicht die Lösung, das hier ist der alte Baum mit der alten Grammatik und was wir stattdessen jetzt brauchen, ist der Baum mit der, also der Pastry mit der verbesserten Grammatik, wieder für den selben Ausdruck, den wir gerade hatten. Und das sieht wie folgt aus. Also das wird die Lösung des kleinen Quizzes. Das sieht so aus, dass wir als Wurzel des Baumes natürlich wieder die Expression haben und das wird jetzt abgeleitet nach Expression, dann additive Operator und Term. Die erste Expression leiten wir ab nach einem Term. Der Term hat in der Mitte ein multiplikativen Operator. Auf der rechten Seite ein Fector und auf der linken Seite einen weiteren Term. Der Term auf der linken Seite ist einfach auch nur ein Fector und alle beide Fectors, die wir hier haben, sind IDs und der multiplikative Operator in unserem Fall ist die, ist die Multiplikation. Der additive Operator hier drüben ist das Plus und dann müssen wir noch den Term da drüben ableiten und den leiten wir auch wieder zu einem Fector ab. Der Schluss endlich der dritte Identifier ist und damit haben wir genau den Baum, der beschreibt, dass unser String tatsächlich von dieser Grammatik erkannt wird, also in der Sprache der arithmetischen Ausdrücke, die wir hier beschreiben wollen, drin ist. Und damit sind wir auch schon am Ende dieser Einheit zum Thema kontextfreie Grammatiken. Sie wissen jetzt also, wie man beschreiben kann, dass bestimmte Konstrukte in einer Programmiersprache legal sind und andere Konstrukte eben nicht legal sind, weil sie nicht von der kontextfreien Grammatik erkannt werden. Und in den nächsten Einheiten schauen wir uns dann an, wie man eigentlich Werkzeuge bauen kann, die eben diese ganzen Ideen von regulären Ausdrücken und dem Scanning der Tokens oder dem Parsing der kompletten Programme implementiert. Bis dahin, vielen Dank fürs Zuhören und bis zum nächsten Mal.