JGU Logo JGU Logo JGU Logo JGU Logo

Institut für Informatik

Michael Wand
Christian Ali Mehmeti-Göpel
Wintersemester 2020/21DIGITAL

Projektaufgabe

Übung 11
Einführung in die Softwareentwicklung




Projekt Übersicht

Letzte Änderung: 01. February 2021, 17:11 Uhr


Exkurs für Interessierte:
Grundlagen Compilerbau in 10 Minuten

Letzte Änderung: 01. February 2021, 17:11 Uhr
keine Punkteim Detail


Im verlinkten Exkurs gibt es einen kurzen Überblick über Grammatiken für formale Sprachen und Compilerbau.
Für die Aufgabe ist es nicht unbedingt nötig, alles durchzuarbeiten — es hilft aber dabei, die Prinzipien hinter den Aufgaben besser zu verstehen.



Aufgabe Parsing — Bäume aus Zeichenketten

Letzte Änderung: 01. February 2021, 17:11 Uhr
18 Punkteim Detail

Worum geht's?
Das ganze Aufgabenblatt beschäftigt sich damit, mathematische Ausdrücke zu manipulieren. Dazu müssen wir diese erstmal in den Rechner bringen.
Wie in der Vorlesung (zu Lehreinheit 8) erklärt, sind Ausdrücke eigentlich Bäume von Operatoren. Dies ist aber relativ schwierig direkt einzugeben (man bräuchte ein komplexes GUI, und die Interaktion wäre immer noch mühsam, wenn man sich nicht eine sehr raffinierte Schnittstelle überlegt). Daher ist es sinnvoll, mathematische Ausdrücke zunächst in linearisierter Form, als einfache Zeichenketten zu repräsentieren. Die Aufgaben, aus einer solchen, linearisierten Kodierung wieder einen Baum zu rekonstruieren, ist in der Informatik als „Parsing“ bekannt. Der Formalismus dahinter ist als kontextfreie Grammatiken/Sprachen bekannt: In jeden Operator im Baum kann man beliebig komplexe, zusammengesetzte Unteroperatoren einsetzen (Teilbäume).


Da es vergleichsweise schwierig ist, "normale" mathematische Ausdrücke mit Klammerung in einen Syntaxbaum zu überführen, gehen wir in dieser Aufgabe von der sogenannten "Umgekehrten Polnischen Notation" aus.


Umgekehrte Polnische Notation
Beispiel: 2 42 sin + x cos *

Aufgabe

Implementierung des Parsers:
Die praktische Aufgabe besteht nun darin den zuvor selbst hergeleiteten Algorithmus zu einer der oben aufgelisteten Syntaxen zu implementieren.


Anforderungen:

  1. Für eine eingegebene Zeichenkette soll ein dazu passender Syntax-Baum zurückgegeben werden.
    Dazu macht es Sinn, Klassen zu definieren, mit denen die Knoten im Operatorenbaum repräsentiert werden können. Hierzu gibt es viele verschiedenen Möglichkeiten. Überlegen Sie sich selbst ein Klassendesign. Um Fehler zu vermeiden, erstellen Sie für jeden Knoten des Baums ein neues Objekt; Knoten mehrfach zu verwenden um speicher zu sparen ist nicht-trivial und verkompliziert die Aufgabe deutlich.
  2. Erklären Sie Ihr Klassendesign & den Parsingablauf stichpunktartig in der README.md.
    (E.g.: Welche Rolle wird durch die jeweiligen Klassen/Programmteile beim Parsen erfüllt?)

    Diese Aufgabe ist nur dazu da, um den KorrektorInnen die Arbeit zu erleichtern. Im Falle, dass die Aufgabe Schwierigkeiten macht, können die KorrektorInnen besser nachvollziehen, wie Ihre Abgabe funktioniert und anteilig Teilpunkte vergeben.
  3. Folgende Features soll ihr Parser erfüllen:
    • Ganze Zahlen (etwa 1, 42 oder 1337)
    • die Operatoren +, - (unär und binär), *, / und ^ (potenzieren)
    • Variablen (etwa x, y, a, b, ...)
    • Vordefinierte Funktionen sin, cos, exp und log
    • Bonus: Fließkommazahlen im Format 123.456789
  4. Bonus: Fertigen Sie ein UML-Diagramm ihrer Klassenhierarchie an
Implementierungstipps
Ein Möglichkeit besteht darin sich direkt auf den vorliegenden String zu stürzen und dann jedes weitere Symbol Zeichen für Zeichen zu extrahieren. Das ist durchaus machbar, kann aber schnell zu unübersichtlichem Code führen, insbesondere, da die Symbole vorliegenden Grammatiken nicht nur aus einzelnen Zeichen bestehen; Zahlen sollen beispielsweise auch aus mehreren Ziffern bestehen können und in der fertigen Implementierung möchten wir auch Funktions- und Operatornamen wie sin oder mod erlauben, die auch aus mehr als nur einem Zeichen bestehen.
Aus dem Grund Teilen wir das Parsen in zwei Schritte auf: die lexikographische Analyse und das Aufbauen des Baumes (auch bekannt als syntaktische Analyse).
Lexikographische Analyse:
Extrahieren Sie dazu zuerst alle im String vorkommenden Tokens und speichern Sie diese in einer Liste. Tokens sind zusammenhängende Folgen von Zeichen, die elementare Sinneinheiten der Sprache bilden: In unserem Fall benötigen wir die folgenden Tokens: Zahlen (beispielsweise ist die ganze Folge von Ziffern, bei Floats auch mit mit Dezimalpunkt, ein Tokenobjekt), Operatorenzeichen (wie +,*,-), Funktionsnamen (sin, cos), Klammern und Variablennamen. Whitespaces (Leerzeichen, Tabs, returns) trennen die Tokens — in der Liste tauchen diese nicht mehr explizit auf. Tokens können aber auch anders getrennt sein (z.B. möchte man, dass in allgemeinen mathematischen Ausdrücken auch 3+4, also ohne Whitespaces, drei Tokens ergibt).
Ein paar Tipps zum Lexer:
  1. Hinweis: Gehen Sie zeichenweise durch die Zeichenkette durch und manipulieren Sie den Laufindex entsprechend des Fundes.
  2. Hinweis: Beginnen Sie damit Leerzeichen, Klammern abzudecken und dann auch Zahlen beliebiger Länge zu erkennen (weiterer Schleifendurchlauf beim Fund einer Ziffer). Konvertieren Sie Zahlenstrings erst im Parser zu Zahlenobjekten.

Der eigentliche Parser
Der Parser bekommt eine Liste von Tokens als Eingabe (er muss sich also nicht mehr um alle kleinen Details kümmern). %Sie sollten nun eine Liste aller der gültigen Tokens in Ihrem String haben (mit Wiederholungen und in der selben Reihenfolge, wie in der Zeichenkette selbst.
Übersetzen Sie diese Tokenliste nun mithilfe Ihres Algorithmus in eine Baumstruktur; jeder Eintrag in der Token-Liste kann nun als ein einzelnes Zeichen erachten werden (das macht die Anpassung des Codes relativ einfach ☺).
Hinweis, falls Sie sich für den schwierigeren Parser entschieden haben: Der ursprüngliche Algorithmus aus Aufgabe 1 hat noch keine Funktionen vorgesehen. Wir können dies leicht umgehen, indem wir einen Dummy-Funktionsoperator definieren und jedes Vorkommen eines Funktionsaufrufes (z.B. sin a) als func Dummyoperator Eingabe auffassen. Indem wir den Dummyoperator schon mit in die Operatorliste nehmen können wir sogar festlegen, wie z.B. ein Ausdruck der Form sin a\textasciicircum 3 übersetzt wird.
Hinweis: Dieser Codeschnipsel könnte etwa bei der Konvertierung von Zahlen helfen:
// string containing the number
string Text = "456";

//number which will contain the result
int Result;

// stringstream used for the conversion constructed with the contents of 'Text' 
// ie: the stream will start containing the characters of 'Text'
istringstream convert(Text);

//give the value to 'Result' using the characters in the stream
//if that fails set 'Result' to 0
if ( !(convert >> Result) )
    Result = 0;

//'Result' now equal to 456


Aufgabe Evaluation

Letzte Änderung: 01. February 2021, 17:11 Uhr
10 Punkteim Detail


  1. Rücktransformation (Unparsing, sozusagen):
    Implementieren Sie nun für alle involvierten Klassen die Methode to_string, die eine Repräsentative Zeichenkette für die (Teil-)Bäume und Objekte zurückgibt. Es muss dabei nicht exakt die gleiche Zeichenkette enstehen, wie die, die eingegeben wurde. Beispielsweise kann sich die neue bis auf Leerzeichen und Klammerungen unterscheiden. Was jedoch der Fall sein sollte ist, dass diese String-Repräsentation selbst wieder einlesbar ist. Insbesondere gilt dann für jede valide Zeichenkette Expression(Expression(str).to_string()).to_string() == Expression(str).to_string()

    Test!
    Prüfen Sie ob Expression(Expression(str).to_string()).to_string() == Expression(str).to_string()

  1. Mathematische Evaluation:
    Außerdem möchten wir einen Ausdruck auswerten, wobei wir für einige Variablen konkrete Werte einsetzen wollen (Binden der Variablen). Überlegen Sie sich, wie sie dies Implementieren könnten.

    Mögliche Umsetzung:
    Strukturell können Sie dies ähnlich zur to_string-Funktionalität gestalten. Noch wissen die Operatorobjekte jedoch nicht, was das Operatortoken eigentlich tut. Übergeben Sie zum Beispiel der Operatordefinition zusätzlich einen Lamba-Ausdruck, der zwei Eingaben erhält und den Operator auf diese beiden anwendet.
    Die Verwendung der Evaluation könnte insgesamt wie folgt aussehen:

    Test!
    Werten Sie die folgenden beiden Ausdrücke aus. Definieren Sie dazu alle nötigen Operatoren und Funktionen. Es sollte (ungefähr) der gleiche Wert rauskommen (wenn nicht, stimmt irgendetwas noch nicht...). \[ \begin{aligned} \sin(x+y) = \sin(x) \cdot \cos(y) + \sin(y)\cdot\cos(x)\\ 4\cdot\cos(x)\cdot\cos(y)\cdot\cos(z) = \cos(x+y-z)+\cos(-x+y+z)+\cos(x-y+z)+\cos(x+y+z) \end{aligned} \]

Aufgabe Symbolische Ableitung

Letzte Änderung: 01. February 2021, 17:11 Uhr
12 Punkteim Detail

Implementieren eine Funktion deriv, die die Ableitung einer einfachen Funktion in gegebenen Variable (z.B. \(x\)) berechnet. Als Operatoren sollen Addition, Subtraktion, Multiplikation und die Funktionen \(\sin(x),\cos(x),\exp(x),\log(x)\) unterstützt werden


Hinweise:


Aufgabe (Bonus) GUI

Letzte Änderung: 01. February 2021, 17:11 Uhr
10 Punkteim Detail

Zum Schluss brauchen wir unbedingt noch etwas hübsche Graphik (Eye Candy muß sein!).


Erstellen Sie eine GUI, bei der der User in einem Textfeld einen mathematischen Ausdruck in einer Variable (x) eingeben kann. Der eingegebene Ausdruck soll dann zusammen mit der passenden Ableitung in der GUI geplottet werden.