Fachbereich Biologie und Informatik Institut für Informatik • Künstliche Intelligenz Prof. Dr. Manfred Schmidt-Schauß Praktikum: Praktische Informatik Anleitung zum Praktikum Sommersemester 2005 Inhaltsverzeichnis 1 Allgemeines 1.1 1.2 3 Organisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.1.1 Scheinvergabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.1.2 Das gesamte Projekt . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.1.3 Tutoren und Gruppen . . . . . . . . . . . . . . . . . . . . . . . . . 4 Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 1.2.1 Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 1.2.2 Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2 Aufgaben 2.1 2.2 2.3 8 Aufgabe 1: Aussagenlogischer Beweiser in Haskell . . . . . . . . . . . . . . 8 2.1.1 Der Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.1.2 Beweiser über Wahrheitstafeln . . . . . . . . . . . . . . . . . . . . . 10 Aufgabe 2: Ein Client-Server-Netzwerk in Haskell und Python . . . . . . . 11 2.2.1 Das Netzwerkprotokoll SPFP . . . . . . . . . . . . . . . . . . . . . 11 2.2.2 Der Client in Python . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.2.3 Der Server in Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Aufgabe 3: Ein graphisches Front-End in Python . . . . . . . . . . . . . . 14 2.3.1 Das Hauptfenster der GUI . . . . . . . . . . . . . . . . . . . . . . . 15 2.3.2 Das Fenster zum Prüfen der Formel . . . . . . . . . . . . . . . . . . 19 3 Hinweise zu einzelnen Themen 3.1 3.2 20 Parser und Parsergeneratoren . . . . . . . . . . . . . . . . . . . . . . . . . 20 3.1.1 Parser und Syntaxanalyse . . . . . . . . . . . . . . . . . . . . . . . 20 3.1.2 Parsergeneratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 3.1.3 Happy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 Objektorientierung in Python . . . . . . . . . . . . . . . . . . . . . . . . . 27 1 3.3 3.4 3.2.1 Syntax der Klassen-Definition . . . . . . . . . . . . . . . . . . . . . 27 3.2.2 Objekte – Instanzen von Klassen . . . . . . . . . . . . . . . . . . . 28 3.2.3 Private Attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 3.2.4 Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 Tkinter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 3.3.1 Was ist Tkinter? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 3.3.2 Ein Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 Netzwerkprogrammierung mit Sockets . . . . . . . . . . . . . . . . . . . . 32 3.4.1 Ein Client-Server-Netzwerk . . . . . . . . . . . . . . . . . . . . . . 32 3.4.2 Was ist ein Socket? . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 3.4.3 Sockets in Python und Haskell . . . . . . . . . . . . . . . . . . . . . 33 2 Kapitel 1 Allgemeines 1.1 Organisation 1.1.1 Scheinvergabe Es sollen je 2–3 Praktikanten für die Bearbeitung einer Aufgabe eine Gruppe bilden. Es ist nicht nötig, dass die Praktikanten alle Aufgaben in dieser Konstellation bearbeiten. Jeder Tutor wird für Fragen, die Präsentation der Arbeiten, die Abgabe der Ausarbeitungen etc. 4 Stunden in der Woche zur Verfügung stehen. Die jeweils erste Stunde davon soll zur Besprechung dienen und Anwesenheit ist daher während dieser ersten Stunde erforderlich. Zur Erfüllung der Aufgaben gehört die Abgabe und Präsentation eines hier auf den Rechnern der RBI laufenden, kommentierten Programms, sowie eine schriftliche Ausarbeitung, die allerhöchstens 15 Seiten lang sein sollte, in der sowohl die Grundlagen, die zur Lösung der Aufgabe notwendig waren dargelegt werden, die Lösung beschrieben wird als auch der Prozeß der Problemlösung protokolliert wird, wozu z.B. auch eine Erfassung der Zeiten für einzelne Tätigkeiten sowie die Dokumentation durchgeführter Tests gehört. Die Ausarbeitungen sind bis jeweils spätestens zum untengenannten Termin abzugeben bzw. die Lösungen sind bis spätestens zu diesem Termin zu präsentieren. 13. Mai 10. Juni 8. Juli Nach Abgabe bzw. Präsentation vergibt der Tutor eine der folgenden Bewertungen für das dokumentierte Programm, die Ausarbeitung und die Präsentation für jeden Teilnehmer: • akzeptiert • nicht akzeptiert • benötigt Nachbesserung Ein Leistungsschein wird nur dann vergeben, wenn für alle Aufgaben akzeptiert“ ver” geben, bzw. nach erfolgter Nachbesserung diese akzeptiert wurde. Die Frist zur Nach3 besserung setzt der Tutor fest (je nach Aufwand), sämtliche Nachbesserungen müssen allerspätestens bis zum 30. Juli 2005 erbracht werden. Wird ein nicht akzeptiert“ vergeben, so gibt es keine Möglichkeit mehr zur Nachbesserung ” und ein Leistungsschein kann nicht vergeben werden. Außerdem sei nochmal erwähnt, dass die Beteiligung bzw. das Stellen von Fragen bei den Besprechungen ausdrücklich erwünscht ist; davon hat nämlich sowohl der Frager, der Tutor als auch die Kommilitonen etwas. 1.1.2 Das gesamte Projekt Die folgende Grafik stellt eine schematische Abbildung des im Praktikum zu realisierenden Projektes dar. Das Projekt gliedert sich in drei Teile, die jeweils als einzelne Aufgabe nacheinander bearbeitet werden. Da die drei Aufgaben nach Vollendung des Praktikums miteinander funktionieren müssen, empfiehlt es sich, die gesamte Praktikumsanleitung am Anfang des Semesters zu lesen. 1.1.3 Tutoren und Gruppen Die einzelnen Aufgaben werden von verschiedenen Tutoren betreut, deren Email-Adressen der folgenden Übersicht zu entnehmen sind: 4 Haskell-Aufgabe F. Abromeit: ppi h [email protected] J. Homann: ppi h [email protected] Netzwerk-Aufgabe T. Elschner: ppi n [email protected] D. Hartmann: ppi n [email protected] Python-Aufgabe J. Bergs: ppi p [email protected] D. Hauser: ppi p [email protected] H. Lokau: ppi p [email protected] Die Teilnehmer werden auf die neun Gruppen A1 – A3, B1 – B3 und C1 – C3 aufgeteilt. Die Zuordnung wird auf der Webseite der Veranstaltung bekannt gemacht. Die zu den Buchstaben zugehörigen Gruppen rotieren alle 4 Wochen, d.h. es gelten die Terminpläne: Terminplan vom 18.04. bis 15.05. Gruppe A1 A2 A3 B1 B2 B3 C1 C2 C3 Aufgabe Haskell (Aufgabe 1) Netzwerk (Aufgabe 2) Python (Aufgabe 3) Haskell (Aufgabe 1) Netzwerk (Aufgabe 2) Python (Aufgabe 3) Haskell (Aufgabe 1) Netzwerk (Aufgabe 2) Python (Aufgabe 3) Termin Montag 9-13 Montag 9-13 Montag 9-13 Donnerstag 9-13 Dienstag 14-18 Donnerstag 9-13 Mittwoch 14-18 Mittwoch 14-18 Mittwoch 14-18 Raum Raum Raum Raum Raum Raum Raum Raum Raum Raum 026 028 027 028 028 027 026 028 027 Tutor J. Homann T. Elschner / D. Hartmann H. Lokau J. Homann T. Elschner / D. Hartmann J. Bergs F. Abromeit T. Elschner / D. Hartmann D. Hauser 026 028 027 028 028 027 026 028 027 Tutor J. Homann T. Elschner / D. Hartmann H. Lokau J. Homann T. Elschner / D. Hartmann J. Bergs F. Abromeit T. Elschner / D. Hartmann D. Hauser Terminplan vom 16.05. bis 12.06. Gruppe A2 A3 A1 B2 B3 B1 C2 C3 C1 Aufgabe Haskell (Aufgabe 1) Netzwerk (Aufgabe 2) Python (Aufgabe 3) Haskell (Aufgabe 1) Netzwerk (Aufgabe 2) Python (Aufgabe 3) Haskell (Aufgabe 1) Netzwerk (Aufgabe 2) Python (Aufgabe 3) Termin Montag 9-13 Montag 9-13 Montag 9-13 Donnerstag 9-13 Dienstag 14-18 Donnerstag 9-13 Mittwoch 14-18 Mittwoch 14-18 Mittwoch 14-18 5 Raum Raum Raum Raum Raum Raum Raum Raum Raum Raum Terminplan vom 13.06. bis 10.07. Gruppe A3 A1 A2 B3 B1 B2 C3 C1 C2 1.2 Aufgabe Haskell (Aufgabe 1) Netzwerk (Aufgabe 2) Python (Aufgabe 3) Haskell (Aufgabe 1) Netzwerk (Aufgabe 2) Python (Aufgabe 3) Haskell (Aufgabe 1) Netzwerk (Aufgabe 2) Python (Aufgabe 3) Termin Montag 9-13 Montag 9-13 Montag 9-13 Donnerstag 9-13 Dienstag 14-18 Donnerstag 9-13 Mittwoch 14-18 Mittwoch 14-18 Mittwoch 14-18 Raum Raum Raum Raum Raum Raum Raum Raum Raum Raum 026 028 027 028 028 027 026 028 027 Tutor J. Homann T. Elschner / D. Hartmann H. Lokau J. Homann T. Elschner / D. Hartmann J. Bergs F. Abromeit T. Elschner / D. Hartmann D. Hauser Programmiersprachen Im Praktikum soll eine Aufgabe mit Haskell, eine mit Python und eine Kommunikation zwischen Haskell und Python programmiert werden. Im folgenden werden wir die Sprachen und deren Interpreter und Übersetzer kurz vorstellen. 1.2.1 Haskell Haskell1 ist eine der zur Zeit wohl bedeutendsten, nicht-strikten, funktionalen Programmiersprachen. Wer sich eingehender in Haskell vertiefen möchte sei auf den Haskell-Report [6] verwiesen, in dem die Sprache definiert wird. Zur Einarbeitung in Haskell ist zum einen das deutsche Buch [1] sowie [5] empfehlenswert, etwas tiefergehend ist [9]. Für Haskell gibt es mittlerweile eine recht große Anzahl von Standardbibliotheken, die hierarchisch organisiert sind. Die Dokumentation der Bibliotheken ist online unter http://www.haskell.org/ghc/docs/6.2.1/html/libraries/index.html zu finden. Zum Haskell-Programmieren stehen uns Hugs und GHC zur Verfügung. Hugs Hugs2 steht für Haskell Users Gofer System und er ist ein Interpreter der Sprache Haskell 98. Es gibt ihn für verschiedene Rechner und Betriebssysteme: darunter Mac, Windows, Unix, Linux. GHC Der Glasgow Haskell Compiler3 hält sich an den Haskell-Report. Im ghc gibt es Erweiterungen, die z.B. die Typklassen betreffen. Der GHC ist fast vollständig in Haskell ge1 2 3 Die offizielle Homepage zu Haskell ist http://haskell.org. Die Homepage von Hugs ist http://haskell.org/hugs/. Die Homepage des GHC ist http://haskell.org/ghc. 6 schrieben und erzeugt C-Code als Zwischencode, der dann von einem auf dem System verfügbaren C-Compiler in ein ausführbares Programm übersetzt wird. Diese Tatsache macht den ghc äußerst portierbar. Für einige Plattformen gibt es zusätzlich einen CodeErzeuger, dessen Verwendung die Übersetzungszeit reduziert, aber nicht zu so gut optimiertem Code führt wie die Verwendung des GNU C Übersetzers. GHC bietet zusätzlich noch den GHCi, der eine interaktive Version des GHC darstellt und ähnlich wie Hugs benutzt werden kann. 1.2.2 Python Python4 ist eine imperative interpretierte Programmiersprache, die über Konzepte der Objektorientierung verfügt. Gute Einführungen in Python geben [2], sowie [10]. Idle Idle5 ist eine in Tkinter geschriebene integrierte Entwicklungsumgebung für Python, die mit der standardmäßigen Distribution von Python mitgeliefert wird. Neben Syntaxhighlighting und weiterer Unterstützung bei der Eingabe von Python-Programmen wie automatische Einrückung und dem kontext-sensitiven Anzeigen von Doc-Strings unterstützt es das Ausführen von Programmen per Knopfdruck sowie das Debuggen von PythonProgrammen. Dokumentation zu Idle ist in [4] zu finden. 4 5 Die offizielle Homepage zu Python ist http://www.python.org. Die Webseite zu Idle ist http://www.python.org/idle/ 7 Kapitel 2 Aufgaben 2.1 Aufgabe 1: Aussagenlogischer Beweiser in Haskell In dieser Aufgabe soll ein aussagenlogischer Beweiser in Haskell geschrieben werden. Die Zielfunktionalität ist ein Modul namens ProofEngine, das die Funktion: proofEngine :: String -> Result exportiert, welche eine textuelle Darstellung einer aussagenlogischen Formel einliest und für sämtliche mögliche Belegungen der aussagenlogischen Variablen prüft, ob diese die Formel erfüllen. Das Ergebnis muss von folgendem Typ sein: data Result = | | Tautology Unsatisfiable Satisfiable [Assignment] type Assignment = [(Variable,Bool)] type Variable = String D.h. die Rückgabe ist • Tautology, falls die Formel eine Tautologie ist; • Unsatisfiable, falls die Formel unerfüllbar ist; • Satisfiable ’Liste aller erfüllenden Belegungen’, falls die Formel keine Tautologie ist, aber erfüllbar ist. Die Aufgabe gliedert sich dabei in folgende Teilaufgaben: • ein Parser 8 • ein Beweiser über Wahrheitstafeln Wir müssen nun eine Einigung über die verschiedenen Datentypen treffen. Diese dienen dann als Darstellung von aussagenlogischen Formeln als Haskelldaten für die Schnittstelle zwischen Parser und Beweiser über Wahrheitstafeln. 2.1.1 Der Parser Der Parser soll mit dem Parsergenerator Happy erzeugt werden (siehe Abschnitt 3.1) Er soll eine Funktion sein, die einen String als Eingabe hat und als Ausgabe die in diesem String kodierte Formel als Haskell-Datentyp. Hierzu müssen wir zunächst definieren, was eine aussagenlogische Formel ist, und wie sie als String dargestellt wird. Aussagenlogische Formeln können mithilfe der (hier mehrdeutigen!) Grammatik Expr ::= | | | | | | Expr ∨ Expr Expr ∧ Expr Expr ⇒ Expr Expr ⇔ Expr ¬ Expr ( Expr ) Var hergeleitet werden, wobei Var Wörter der durch den regulären Ausdruck (a + b + . . . + z + A + B + . . . + Z)+ erzeugten Sprache sind. Außerdem nehmen wir die folgenden Assoziativitäten an: • ∨, ∧ sind links-assoziativ. • ⇒ ist rechts-assoziativ. • ⇔ ist nicht assoziativ. Für die Präzedenzen gilt: ¬ vor ∧ vor ∨ vor ⇒ vor ⇔. Z. B. entspricht der Ausdruck ¬A ∧ B ∨ C ⇔ A ⇒ C dem geklammerten Ausdruck (((¬A) ∧ B) ∨ C) ⇔ (A ⇒ C). Da der ASCII-Zeichensatz die für unsere Formeln benötigten Sonderzeichen nicht bereit stellt, müssen wir eine Darstellung dieser Zeichen als ASCII-String definieren. Hierbei benutzen wir andere Sonderzeichen, die der ASCII-Zeichensatz enthält, bzw. für die Implikation und die Äquivalenz Zeichenfolgen: 9 Operation Negation Implikation Oder Und Äquivalenz mathematisches Symbol ¬ ⇒ ∨ ∧ ⇔ textuelle Darstellung => | & <=> Der Parser soll jetzt also einen String, der eine aussagenlogische Formel darstellt, einlesen und in ein internes Syntaxformat der Formel übersetzen. Dabei soll es erlaubt sein Leerzeichen, Zeilenendemarken und ähnliches zu überlesen. Der erzeugte Syntaxbaum wird dann an den Beweiser übergeben. Eine Eingabe kann z.B. so aussehen: A & -A => B Wie wir gesehen haben, sind aussagenlogische Formeln Terme, die ein übergeordnetes Funktionszeichen (einen Junktor) mit aussagenlogischen Formeln als Argumente haben. Wir können diese Terme in einem rekursiven Typ als Bäume darstellen. Hierfür definieren wir uns einen Datentypen in nahe liegender Weise: data Formula = Var Variable | Neg Formula | Impl Formula Formula | Equi Formula Formula | And Formula Formula | Or Formula Formula Ein Beispiel für eine Formel ist: f1 = Neg (Impl (And (Variable "A") (Neg (Variable "A"))) (Variable "B")) 2.1.2 Beweiser über Wahrheitstafeln Das Verfahren, Wahrheitstafeln für aussagenlogische Formeln aufzustellen, sollte bekannt sein. Die Aufgabe besteht also darin, die Eingabe zu parsen und für jede Belegung der Variablen, die in der Eingabe auftreten den Wert des Ausdrucks zu berechnen. • Ist die Formel für keine der Belegungen wahr, so ist die Formel unerfüllbar; • ist die Formel für alle Belegungen wahr, so handelt es sich um eine Tautologie; 10 • ist die Formel nur für manche Belegungen wahr, so ist sie erfüllbar. Erzeugen Sie geeignete Eingaben, um Ihr Programm zu testen und sorgen Sie dafür, dass es mit den auf den WWW-Seiten des Praktikums gegebenen Tests geprüft werden kann. Halten Sie sich insbesondere an die Schnittstellendefinition für die Funktion proofEngine und benutzen Sie den geforderten Modulnamen ProofEngine, denn in Aufgabe 2 wird gerade diese Funktion importiert. 2.2 Aufgabe 2: Ein Client-Server-Netzwerk in Haskell und Python Innerhalb dieser Aufgabe sollen erstellt werden: • Ein Client, implementiert in Python, der von der in Aufgabe 3 implementierten GUI aufgerufen wird und die aussagenlogische Formel via Sockets an einen Server sendet, auf die Antwort wartet und diese in aufbereiteter Form schließlich als Ergebnis (an die GUI) liefert. • Ein Server, implementiert in Haskell, der Anfragen in Form von booleschen Formeln annimmt, für jeden Auftrag den Beweiser aus Aufgabe 1 mit der erhaltenen Formel aufruft und das Ergebnis zurück an den Client schickt. 2.2.1 Das Netzwerkprotokoll SPFP Wir verwenden ein selbst definiertes und sehr einfaches Protokoll, welches den Namen simple propositional formula protokoll (SPFP) hat. Es sei durch folgende BNF-artige Grammatik definiert: P ::= | | | | PROPF NoChars Formula TAUTO UNSAT ERROR NoChars ErrMessage SATIS NoChars FormattedModels wobei: • genau ein Leerzeichen ist. • NoChars die Anzahl der Zeichen von Formula, FormattedModels bzw. ErrMessage ist. Durch diese Angabe ist stets eindeutig, wie viele Zeichen vom Socket noch gelesen werden müssen. 11 • Formula ist eine Zeichenkette, die die aussagenlogische Formel darstellt. • FormattedModels ist eine Zeichenkette, die die erfüllenden Belegungen einer Formel bereits formatiert enthält. Formatiert bedeutet hierbei, dass der String direkt ausgedruckt werden kann und dabei eine ordentliche“ Darstellung der erfüllenden ” Belegungen angezeigt wird. • ErrMessage ist eine Zeichenkette, die eine Fehlermeldung enthält. 2.2.2 Der Client in Python Es ist ein Modul namens SPFPclient.py zu implementieren. Es stellt u.a. die Funktion requestProof zur Verfügung, die ein Objekt der Klasse Request übergeben bekommt, und als Ergebnis einen String liefert. Diese Funktion wird von der graphischen Benutzeroberfläche aus Aufgabe 3 importiert und aufgerufen, sie dient als Schnittstelle zwischen der GUI und dem Netzwerkclient. Die Klasse Request wird im Modul sharedTools definiert, welches über die WWW-Seite zum Praktikum erhältlich ist. Ein Objekt der Klasse hat die Attribute formula : enthält die aussagenlogische Formel als String. Der Zugriff auf das Attribut sollte über die Objektmethoden getFormula und setFormula geschehen. type : Der Typ der Anfrage, er hat einen der Werte • "SATIS", d.h. die Formel soll auf Erfüllbarkeit geprüft und die Liste aller erfüllenden Belegungen zurück gegeben werden. • "TAUTO", d.h. es soll geprüft werden, ob die Formel eine Tautologie ist. Die möglichen Antworten sind Ja oder Nein. • "UNSAT", d.h. es soll geprüft werden, ob die Formel unerfüllbar ist. Die möglichen Antworten sind Ja oder Nein. Der Zugriff auf das Attribut sollte über die Objektmethoden getType und setType geschehen. ip Hier ist die IP-Adresse des Servers, an den die Anfrage gestellt werden soll, als String gespeichert. Die Zugriffsmethoden sind getIP und setIP. port Hier ist die Port-Nummer des Servers, an den die Anfrage gestellt werden soll, als String gespeichert. Die Zugriffsmethoden sind getPort und setPort. Die Aufgabe des Clients ist es nun, über die im Objekt der Klasse Request gespeicherten Netzwerkdaten (IP-Adresse und Port) eine Anfrage (via Sockets) an den Server zu stellen, d.h. die Formel an diesen in Form des oben beschriebenen Netzwerkprotokolls zu senden. Die erhaltene Antwort muss dann mit dem geforderten Anfragetyp (Erfüllbarkeit, Tautologie, Unerfüllbarkeit) abgeglichen werden und ein formatierten String zurück gegeben werden, den der GUI-Client ohne weitere Formatierung anzeigen kann. 12 Man beachte, dass z.B. folgendes Szenario auftreten kann: Das Objekt der Klasse Request stellt eine Frage nach Tautologie, die Antwort des Servers ist SATIS 103 ..., nun ist der Rückgabewert der Funktion requestProof mit der Anfrage abzugleichen, also in etwa Die Formel ist keine Tautolgie. 2.2.3 Der Server in Haskell Es ist ein Server zu implementieren, der via Stream-Sockets Anfragen der Clients entgegen nimmt, die boolesche Formel mittels dem Beweiser aus Aufgabe 1 prüft (hierfür importiert er die Funktion proofEngine aus dem Modul ProofEngine, welches in einer Dummy-Implementierung auch über die WWW-Seite zum Praktikum erhältlich ist), und schließlich sendet er das Ergebnis in Form des SPFP-Protokolls an den Client. Beachte, dass der Datentyp Result im Modul TypeDefs definiert ist, welches über die WWW-Seite zum Praktikum erhältlich ist. Außerdem sei angeraten die erste Seite von Aufgabe 1 zu studieren, denn dort wird die Semantik des Datentyps Result dargestellt. Vor dem Senden des Ergebnisses ist im Falle, dass die Formel erfüllbar ist, d.h. der Server erhält einen Ausdruck der Form Satisfiable assignments vom Beweiser, eine textuelle Aufbereitung der erfüllenden Belegungen vorzunehmen. Z.B. für die Formel A => B ist die Rückgabe: Satisfiable [[("A",True),("B",True)],[("A",False),("B",True)],[("A",False),("B",False)]] Es sollte dann ein String der Form: "A\t-> Wahr\n B\t-> Wahr ----------------------------------------------------------------------\n A\t-> Falsch\n B\t -> Wahr\n ----------------------------------------------------------------------\n A\t-> Falsch\n B\t-> Falsch\n ----------------------------------------------------------------------\n" als FormattedModels im SPFP-Protokoll gesendet werden. Ausgedruckt ergibt dieser String eine schöne Darstellung der Belegungen: A -> Wahr B -> Wahr ---------------------------------------------------------------------A -> Falsch B -> Wahr ---------------------------------------------------------------------13 A -> Falsch B -> Falsch ---------------------------------------------------------------------Weitere Anforderungen an den Server sind: • Fehler die beim Aufruf des Beweiser entstehen können (insbesondere Fehler beim Parsen) sollen abgefangen werden und dann eine Fehlermeldung, mittels dem SPFPProtokoll an den Client gesendet werden. Zum Abfangen der Fehler steht auf der WWW-Seite des Praktikums das Modul CatchErrors bereit, das die Funktion resultOrError exportiert, welche die Typsignatur resultOrError:: a -> IO (Maybe a) hat. Die Semantik ist hierbei: Der im ersten Parameter übergebene Ausdruck wird ausgewertet. Falls diese Auswertung fehlerlos verläuft ist das Ergebnis eine IOAktion die Just b zurückliefert, wobei b der Wert des Ausdrucks ist. Treten Fehler auf, so wird als Ergebnis eine IO-Aktion geliefert, die Nothing zurück gibt. Die Funktion sollte so verwendet werden, dass als Argument, der Aufruf des Beweisers mit seinem Argument übergeben wird. • Der Server sollte mehrere Client-Anfragen gleichzeitig bearbeiten können. Hierfür ist Nebenläufigkeit erforderlich. Die Standardbibliothek Control.Concurrent stellt die Funktion forkIO zur Verfügung. Diese nimmt eine IO-Aktion als Argument und führt diese IO-Aktion in einem nebenläufigen Thread aus. Es bietet sich also an, diese Funktion zur Realisierung der Nebenläufigkeit zu verwenden. • Der Server sollte möglichst robust sein, d.h. Anfragen die nicht dem Protokoll entsprechen, sollten nicht dazu führen, dass das Server-Programm abstürzt. 2.3 Aufgabe 3: Ein graphisches Front-End in Python In dieser Aufgabe soll eine graphische Benutzeroberfläche zur Eingabe, Abspeicherung und Übermittlung an den Beweiser aus Aufgabe 1 (unter Verwendung des Clients aus Aufgabe 2) einer aussagenlogischen Formel in Python mithilfe der Tkinter-Bibliothek erstellt werden. Im folgenden werden die geforderten Funktionalitäten anhand von beispielhaften Bildern spezifiziert. Für die Lösung der Aufgabe gilt: • Die erstellte GUI darf ruhig hinsichtlich der Positionierung und des Erscheinungsbildes abweichend von diesen Bildern implementiert werden. Die geforderten Funktionalitäten müssen jedoch vorhanden und mithilfe von Tkinter und der erlaubten Zusatzbibliothek Pmw erstellt worden sein. 14 • Der Entwurf sollte strukturiert mit dem Mittel der Objektorientierung geschehen. Z.B. ist denkbar, dass die Werkzeugleiste als Unterklasse der Klasse Frame implementiert wird. 2.3.1 Das Hauptfenster der GUI In der folgenden Abbildung ist das Hauptfenster der zu erstellenden Applikation dargestellt Titel Menüleiste Werkzeugleiste Scrollleiste Eingabefeld für die Formel Knopf zum Checken der Formel Knopf zum Löschen der Eingabe Knöpfe zur Eingabe binärer Operatoren Knopf zur Eingabe des Negations−Operators Knopf zur Eingabe von Variablen Knopf zur Eingabe von Klammern Das Hauptfenster sollte einen eigenen vom Standard abweichenden Titel in der Titelleiste anzeigen, die weiteren Komponenten des Hauptfensters werden in den folgenden Abschnitten beschrieben. Die Menüleiste Die Menüleiste soll mindestens die Hauptmenüs File“, Check“, Configure“ und Info“ ” ” ” ” enthalten. Die einzelnen Menüeinträge und deren Funktionalitäten sind: File New Erstellen einer neuen Datei. 15 Open Öffnen einer bestehenden Datei. Save Speichern der aktuell bearbeitenden Datei. Save as ... Speichern der aktuell bearbeitenden Datei unter einem neuen Namen. Exit Verlassen der Anwendung. Check Formula Öffnet einen Dialog zum Absenden der Formel und des gewünschten zu prüfenden Prädikats. (siehe Abschnitt 2.3.2) Configure Connection Öffnet einen Dialog zum Einstellen der Verbindungsdaten, diese bestehen aus der IP-Adresse und dem Port des Servers, auf dem der Beweiser läuft. Ein beispielhaftes Dialogfenster für diese Einstellung ist in der folgenden Abbildung dargestellt: Info About Öffnet ein Info-Fenster, das Informationen über Version, Author und Kontaktinformationen anzeigt. Dies könnte für die Professur KIST wie folgt aussehen: Für die Implementierung der Menüleiste kann das Menu-Widget von Tkinter oder auch das Megawidget“ MenuBar der Tkinter-Erweiterung Pmw zur Implementierung benutzt ” werden. Für die Untereinträge Open“ und Save as ...“ sind Dialoge notwendig, um den Da” ” teinamen und den Dateipfad zu ermitteln. Solche Dialoge gibt es bereits als Widget der 16 Tkinter Bibliothek, diese können verwendet werden. Die folgenden Abbildungen zeigen die zwei Dialoge. Bei Save“ im Untermenü File“ sollte nach einem Dateinamen gefragt werden, falls die ” ” momentan bearbeitete Datei noch keinen Namen besitzt. Zum Lesen und Schreiben auf Dateien stellt Python die Funktion open, close und read und write zur Verfügung. Ein gute Anleitung hierzu bietet Kapitel 7 des PythonTutoriums (siehe [10]). Die Werkzeugleiste Die Werkzeugleiste sollte als eigener Frame mit 7 Knöpfen implementiert werden, wobei die einzelnen Knöpfe die folgende Funktionalität bereitstellen: • Erzeugen einer neuen Datei • Öffnen einer vorhandenen Datei • Speichern der aktuell bearbeitenden Datei • Speichern der aktuell bearbeitenden Datei unter einem anderen Namen • Ausschneiden von markierten Text aus dem Eingabefeld für die Formel und Ablage des Textes in der Zwischenablage des Windowmanagers. • Kopieren von markiertem Text aus dem Eingabefeld in die Zwischenablage des Windowmanagers. • Einfügen des Inhalts der Zwischenablage des Windowmanagers in das Eingabefeld für die Formel. Die ersten vier Funktionalitäten entsprechen den Funktionalitäten New“, Open“, Save“ ” ” ” und Save as ...“, wie sie im Menü File“ der Menüleiste gegeben sind. ” ” Die letzten drei Funktionalitäten benutzen die Zwischenablage des Windowmanagers, was zunächst schwierig zu implementieren scheint. Hierbei sei jedoch erwähnt, dass das Widget Entry bereits solche Funktionalitäten für das Drücken von Tastenkombinationen bereitstellt, die relativ leicht wiederverwendet werden können. 17 Das Eingabefeld für die Formel Das Eingabefeld für die aussagenlogische Formel kann als Entry- oder als Text-Widget implementiert werden und soll mit einer Scroll-Leiste verbunden werden, so dass auch lange Formeln bequem editiert und betrachtet werden können. Zusätzlich soll ein Knopf existieren, der die Funktionalität bietet, den gesamten Text des Eingabefeldes zu löschen. Ein weiterer Knopf soll dem Aufruf des Dialoges zum Prüfen der Formel (siehe Abschnitt 2.3.2) dienen. Dieser Knopf entspricht in der Funktionalität dem Menüeintrag Formula“ ” aus dem Menü Check“. ” Knöpfe zur Eingabe von Operatoren, Klammern und Variablen Das Hauptfenster soll noch weitere Knöpfe zum Editieren der Formel besitzen. Dies sind • vier Knöpfe zum Einfügen der binären Operatoren Und (&), Oder (|), Implikation (=>) und Äquivalenz (<=>), • ein Knopf zum Einfügen des unären Negationsoperator (-) , • ein Knopf zum Einfügen runder Klammern, • ein Knopf um Variablennamen einzufügen, hierfür muss der einzufügende Variablenname mittels eines Dialogs abgefragt werden. Ein solcher Dialog ist Sämtliche mit den Knöpfen verbundene Aktionen fügen in das Eingabefeld für die Formel ein oder mehrere Zeichen ein. 18 2.3.2 Das Fenster zum Prüfen der Formel Das Fenster zum Prüfen der Formel sollte zum einen die eingegebene Formel darstellen, zum anderen durch Radio-Knöpfe eine Auswahl gestatten, welches Prädikat für die Formel verwendet werden soll. Die drei folgenden Prädikate sind dabei vorgesehen: • Ist die aussagenlogische Formel eine Tautologie? • Ist die aussagenlogische Formel erfüllbar? • Ist die aussagenlogische Formel unerfüllbar? Desweiteren sollte ein Knopf vorhanden sein, um das Prüfen der Formel abzubrechen (Cancel), und ein weiterer Knopf um die Anfrage an den Server zu senden. Hierbei wird ein Aufruf der Funktion requestProof aus dem Modul SPFPClient abgesetzt, welche in Aufgabe 2 implementiert wird. Dieser Funktion wird ein Objekt der Klasse Request übergeben, die im Modul sharedTools definiert ist und über die Webseite zum Praktikum erhalten werden kann. Man beachte, dass das Request-Objekt innerhalb dieser Aufgabe erzeugt werden muss. Als Ergebnis des Aufrufes von requestProof wird ein String zurück geliefert, dieser muss in einem Ausgabefenster bzw. in einem zusätzlichen Textfeld des Fensters zum Prüfen der Formel angezeigt werden. 19 Kapitel 3 Hinweise zu einzelnen Themen 3.1 Parser und Parsergeneratoren In der reinen Haskell-Aufgabe (Beweisen mit Hilfe von Wahrheitstabellen) soll der Parsergenerator Happy1 für das Parsen des Eingabestrings verwendet werden. Dieses Kapitel beschreibt, was ein Parser ist und motiviert die Verwendung von Parsergeneratoren in der Softwareentwicklung. Am Schluß wird auf die Benutzung des Happy eingegangen. 3.1.1 Parser und Syntaxanalyse Was ist ein Parser? Wer einen Compiler für eine Programmiersprache entwickeln möchte, steht u.a. vor der Aufgabe, eine Funktion zu schreiben, welche den Quellcode auf syntaktische Korrektheit überprüft. Der Quellcode ist zunächst nichts anderes als eine sehr lange Folge von ASCII-Zeichen ohne inhaltliche Bedeutung. Damit dieser String als ein korrektes Programm erkannt werden kann, muss die Anordnung der Zeichen gewissen Regeln genügen. Die Gesamtheit dieser Regeln wird als Syntax der Programmiersprache bezeichnet. Ein Programm, welches die Syntaxanalyse durchführt, nennt sich Parser. Man kann sich nun viele Beispiele ausdenken, wo auch in anderen Gebieten — also nicht nur im Bereich des Compilerbaus — Parser eingesetzt werden können. Ein solches Beispiel ist eben gerade die Überprüfung eines aussagenlogischen Ausdrucks. Ein anderes Beispiel wird im nächsten Unterabschnitt vorgestellt. Beispiel: einfacher Taschenrechner Wir wollen nun an einem einfachen Beispiel sehen, wie man zu einem gegebenen Problem eine exakte Syntaxbeschreibung angibt. Die Notation, welche wir hier verwenden, nennt sich Backus Naur Form oder kurz: BNF. 1 Verfügbar unter http://haskell.org/happy 20 Die Syntax wird in Form von Ableitungsregeln angegeben. Dabei stehen auf der linken Seite einer Regel Variablennamen (Nonterminals), auf der rechten Seite stehen Strings aus weiteren Variablen und Symbolen aus dem Eingabestrom des Parsers (Terminals). Der Parser versucht nun ausgehend von einer Startvariablen solange Ableitungen durchzuführen, bis ein String aus Terminals entstanden ist, der mit dem Eingabestring identisch ist. Nehmen wir an, unsere Syntax (in BNF) lautete: S ::= a | aX X ::= Xb | ε wobei S das Startsymbol bezeichne, wir davon ausgehen, dass Nonterminals fett und groß und Terminals klein geschrieben werden, das Zeichen | eine Art Oder“ und ε das ” leere Terminal“ darstellt. Dies ist die Syntax der formalen Sprache {a, ab, abb, ...}. Die ” Ableitung des Wortes abb sieht dann z.B. wie folgt aus: S → aX → a(bX) → a(b(bX)) → a(b(bε)) Als ein etwas komplexeres Beispiel betrachten wir einen Taschenrechner, welcher die vier Grundrechenarten beherrscht. Unser erster Ansatz wird wohl wie folgt lauten: Expr ::= | | | | | Zahl ::= Ziff ::= Zahl ( Expr ) Expr + Expr Expr − Expr Expr ∗ Expr Expr / Expr Ziff | Zahl Ziff 1 | 2 | ... | 9 | 0 d.h. Zahlen werden aus einzelnen Ziffern gebildet (wir verzichten auf negative Zahlen) und ein Rechenausdruck ist eine Zahl oder ist aus Unterausdrücken der gleichen Form zusammengesetzt. Dies sieht recht einleuchtend aus, aber es ist nicht wirklich das was wir wollen: 2+5∗3 Dieser Eingabestring läßt sich auf verschiedene Arten ableiten: Expr → Expr + Expr → ... → 2 + Expr → 2 + (Expr ∗ Expr) → ... Expr → Expr ∗ Expr → ... → Expr ∗ 3 → (Expr + Expr) ∗ 3 → ... Die zweite Ableitung entspricht nicht dem, was wir von der Mathematik her kennen ( Punkt-vor-Strich Regel“). Wir werden also diese zusätzlichen Regeln in irgend” einer Weise in unsere BNF einbauen müssen. Die folgende Syntax erfüllt die bekannten 21 Präzedenzregeln: Expr Term Fact Zahl Ziff ::= ::= ::= ::= ::= Expr + Term | Expr − Term | Term Term ∗ Fact | Term/Fact | Fact ( Expr ) | Zahl Ziff | Zahl Ziff 1 | 2 | ... | 9 | 0 Es braucht ein wenig Übung zu verstehen, wie und warum dies funktioniert. 3.1.2 Parsergeneratoren Was ist ein Parsergenerator? Nachdem man für eine Sprache eine Syntax in Form einer BNF erstellt hat, geht es an die Umsetzung der Syntax in die Zielsprache. Je nach Komplexität der Syntax wird man diese Aufgabe irgendwo zwischen lästig‘ und unzumutbar‘ einordnen. Auch sieht man ’ ’ dem geschriebenen Quelltext oft kaum mehr die ursprüngliche Syntax an. An dieser Stelle kommen Parsergeneratoren ins Spiel. Ein Parsergenerator ist ein Programm, welches als Eingabe eine Syntax in BNF2 erhält und daraus einen Parser in der Zielsprache — in unserem Fall also Haskell — erzeugt. Dadurch konzentriert sich die Arbeit im wesentlichen auf das Finden einer Syntax, und die Fehler die auftreten sind i.a. logischer Natur (keine Programmierfehler). Der bekannteste Parsergenerator ist Yacc,3 welcher in den 70er Jahren für die Herstellung der Parser in UNIX C-Compilern entwickelt wurde und vielen anderen Parsergeneratoren (z.B. dem Happy), zumindest was die Notation betrifft, als Vorbild gedient hat. Lexikalische Analyse Wir hatten gesagt, dass es sich bei der Eingabe eines Parsers um einen String von Zeichen handelt — üblicherweise sind dies ASCII-Zeichen. Dabei gibt es aber ein paar Dinge zu beachten. Betrachten wir dazu folgenden Haskell-Text: > func var = 5 * var Es ist offenbar unerheblich, ob beispielsweise zwischen den Zeichen 5 und * kein, ein oder beliebig viele Freizeichen stehen. Auf der anderen Seite ergibt sich ein völlig anderes Programm, wenn wir schreiben: > func v ar = 5 * v ar Wir müssen also unterscheiden zwischen den ASCII-Zeichen, wie sie uns im Eingabestring begegnen und den Zeichen, die aus der Sicht der Syntax Sinneinheiten bilden. 2 3 bzw. einer der BNF ähnlichen Notation, wie im Beispiel des Happy steht für Yet another Compiler Compiler 22 Solche Sinneinheiten sind im obigen Beispiel die Zeichen“ func, var, =, 5, * und var. ” Keine Sinneinheiten sind dagegen die Zwischenräume und der abschließende Wagenrücklauf (’\n’).4 Man nennt diese Sinneinheiten Token — es ist die Aufgabe eines Lexers, aus dem Strom von ASCII-Zeichen eine Folge von Token zu generieren, um den Parser von technischen Details zu entlasten. Eine solche Tokenfolge könnte für obiges Beispiel wie folgt aussehen:5 [BEZEICHNER func, BEZEICHNER var, ZUWEISUNG, INT 5, MULT, BEZEICHNER var] Eine lexikalische Analyse ist nun ein Pattern Matching ähnlich dem, wie wir es von Haskell her kennen. D.h. auf der linken Seite stehen Muster, denen gewisse Folgen von ASCIIZeichen entsprechen können. Diese Muster sind reguläre Ausdrücke. Auf der rechten Seite stehen Token, die im Falle eines Matchings erzeugt werden sollen. Einige Beispiele sollen dies klarmachen: * (0-9)+ TokenTimes TokenInt $$ Die erste Zeile ordnet dem Zeichen *‘ das entsprechende Token TokenTimes zu. In der ’ zweiten Zeile werden Zahlen aus Ziffern erkannt. $$ steht hierbei für den Teilstring, der gematcht wurde, also z.B. 100. Das Programm Alex6 ist wie bereits erwähnt ein Lexer-Generator. Da man aber den ASCII-Strom häufig recht einfach von Hand“ in den zugehörigen Tokenstrom überführen ” kann, wird ein solches Programm seltener benutzt als ein Parsergenerator. 3.1.3 Happy Happy7 ist ein Parsergenerator, dessen Zielsprache Haskell ist. Er hat viel von Yacc übernommen. Die offizielle Referenz ist [8]. Einen Lexer-Generator gibt es zwar auch für Haskell (Alex), aber es zeigt sich, dass es gerade in Haskell durch dessen ausgefeiltes Patternmatching selten nötig ist, eine solche Software zu verwenden. Stattdessen schreibt man einen Lexer gewöhnlich von Hand als ein eigenständiges Modul und bindet dieses mit dem Befehl import in das Happy-Skript ein (s. nächsten Unterabschnitt). Aufbau eines Happy-Skripts In diesem Abschnitt werden wir beschreiben, wie eine Parserspezifikation für Happy aussieht. Hierfür verwenden wir als Beispiel arithmetische Ausdrücke, mit der (mehrdeuti4 5 6 7 Diese Zeichen werden auch als Whitespace“ bezeichnet. ” wobei wir gleich eine Schreibweise wählen, wie wir sie auch in Haskell benutzen würden, vorausgesetzt ein Datentyp Token existierte. http://haskell.org/alex/ http://haskell.org/happy/ 23 gen!) Grammatik: Expr ::= | | | | | Expr + Expr Expr − Expr Expr ∗ Expr Expr / Expr ( Expr ) Zahl Die Produktionen für Zahl geben wir nicht an, da wir das eigentlich Parsen der Zahl dem Lexer überlassen werden. Jedes Happy-Skript besteht aus bis zu 4 Teilen. Der erste (optionale) Teil ist ein Block Haskell-Code, der von geschweiften Klammern umschlossen wird. Dieser Block wird unverändert an den Anfang der durch Happy generierten Datei gesetzt. Für gewöhnlich stehen hier der Modulkopf, Typdeklarationen, import-Befehle usw. { module Calc where import Char } Der nächste Teil enthält verschiedene Direktiven, die Happy für eine korrekte Funktionsweise unbedingt benötigt: • %name NAME bezeichnet den Namen der Parserfunktion. Unter diesem Namen kann der Parser also später aufgerufen werden. • %tokentype { TYPE } Dies ist der Ausgabetyp des Lexers und damit der Eingabetyp des Parsers. • %token MATCHLIST Hier werden den Token, die vom Lexer erzeugt wurden, die Terminals zugewiesen, die in der BNF verwendet werden. Ein Beispiel ist: %name calculator %tokentype { Token %token int ’+’ ’-’ ’*’ ’/’ ’(’ ’)’ } { { { { { { { TokenInt $$ } TokenPlus } TokenMinus } TokenTimes } TokenDiv } TokenOB } TokenCB } 24 Der Parser wird somit den Namen calculator erhalten, die verwendeten Tokens sind vom Datentyp Token und für die Zuweisung der Terminals an die Tokens gilt: Links stehen die Terminals, rechts in geschweiften Klammern die Token. Das Symbol $$ ist ein Platzhalter, das den Wert des Tokens repräsentiert. Normalerweise ist der Wert eines Tokens der Token selbst, mit $$ wird ein Teil des Tokens als Wert spezifiziert. Im Beispiel ist der Wert des Tokens TokenInt zahl die Zahl. Es schließt sich der Grammatikteil an (vom zweiten Teil durch ein %% getrennt), in dem also in einer BNF ähnlichen Notation die Syntax, wie man sie sich zuvor überlegt hat, aufgeschrieben wird. Auf die kleinen Unterschiede zur BNF möchte ich hier nicht eingehen, wichtiger ist, dass man hinter jede Regel eine sogenannte Aktion schreiben kann. %% Expr :: { Expr } Expr : Expr ’+’ Expr | Expr ’-’ Expr | Expr ’*’ Expr | Expr ’/’ Expr | ’(’ Expr ’)’ | int { { { { { { Plus $1 $3} Minus $1 $3} Times $1 $3} Div $1 $3 } $2 } Number $1} Hinter den Regeln steht in geschweiften Klammern jeweils ein Stück Haskell-Code. Dies sind Aktionen“, die immer dann ausgeführt werden, wenn diese Regel abgeleitet wird. ” Mittels $i wird auf den Wert von i-ten Terminals bzw. Nonterminals zugegriffen. Der Wert eines Terminals ist dabei normalerweise das Terminal selbst. Durch die Aktionen hat der Parser also eine Ausgabe (und ist nicht nur ein reiner Syntax-Überprüfer). In unserem Beispiel ist die Ausgabe eine Objekt vom Typ Expr. Wie wir nun schon sehen, werden die Zahlen nicht mittels der Grammatik geparst, sondern direkt vom Token TokenInt bzw. Terminal int übernommen. Dies ist eine Vereinfachung, d.h. wir überlassen das korrekte Parsen der Zahlen dem Lexer (er erstellt ja das Token TokenInt). Der vierte Teil eines Happy-Skripts ist wieder ein in geschweifte Klammern gesetzter Block mit Haskell-Code, welcher unverändert ans Ende der erzeugten Datei gesetzt wird. Hier muss zumindest die Funktion happyError stehen, welche im Fall eines Syntax-Fehlers von der Parser-Funktion automatisch angesprungen wird (damit dies funktioniert, darf für diese Funktion kein anderer Name verwendet werden.) Oft ist hier auch der Lexer implementiert, wenn der Programmierer zu faul war, ihn in ein eigenes Modul zu stecken. Für unser Beispiel müssen auch die Datentypen Expr und Token sowie der Lexer irgendwo definiert werden, d.h. entweder in einem der Haskell-Code-Abschnitte der Parserspezifikationsdatei oder in externen Haskell-Dateien, die dann importiert werden. Der Vollständigkeit halber, der Rest der der Parserspezifikation für unser Beispiel: { happyError :: [Token] -> a happyError _ = error "parse error!" 25 data Token = | | | | | | data Expr TokenInt Int TokenPlus TokenMinus TokenTimes TokenDiv TokenOB TokenCB = Plus | Minus | Times | Div | Number deriving(Show) Expr Expr Expr Expr Int Expr Expr Expr Expr lexer :: String -> [Token] lexer [] = [] lexer (’+’:cs) = TokenPlus : lexer cs lexer (’-’:cs) = TokenMinus : lexer cs lexer (’*’:cs) = TokenTimes : lexer cs lexer (’/’:cs) = TokenDiv : lexer cs lexer (’(’:cs) = TokenOB : lexer cs lexer (’)’:cs) = TokenCB : lexer cs lexer (c:cs) | isSpace c = lexer cs | isDigit c = lexNum (c:cs) | otherwise = error ("parse error, can’t lex symbol " ++ show "c") lexNum cs = TokenInt (read num) : lexer rest where (num,rest) = span isDigit cs } Mit der so erstellten Parserspezifikation (die Dateien haben die Endung .y bzw .ly falls es sich um ein literate skript handelt), kann nun mittels happy der Parser generiert werden: happy example.y shift/reduce conflicts: 16 Die Meldung der Konflikte sagt uns, dass etwas nicht stimmt. Der erstellte Parser weiß in manchen Situationen nicht was er tun soll. Der Grund hierfür liegt in der Mehrdeutigkeit unserer Grammatik. Wir könnten nun eine eindeutige (aber auch komplizierte Grammatik) benutzen, aber happy bietet uns die Möglichkeit Präzedenz und Assoziativität von Operatoren am Ende der Direktiven festzulegen. Hierbei gilt • %left Terminal(e) legt fest, dass diese Terminale links-assoziativ sind (d.h. ein Ausdruck a ⊗ b ⊗ c wird als (a ⊗ b) ⊗ c aufgefasst). 26 • %right Terminal(e) legt fest, dass diese Terminale rechts-assoziativ sind (d.h. ein Ausdruck a ⊗ b ⊗ c wird als a ⊗ (b ⊗ c) aufgefasst). • %nonassoc Terminal(e) legt fest, dass diese Terminale nicht assoziativ sind (d.h. ein Ausdruck a ⊗ b ⊗ c kann nicht geparst werden und es tritt ein Fehler auf) Die Präzedenz der Terminale gegenüber den anderen Terminalen wird durch die Reihenfolge %left, %right und %nonassoc Direktiven festgelegt, wobei früher“ weniger ” ” Präzedenz“ bedeutet. Nach dem Einfügen der Zeilen %left ’+’ ’-’ %left ’*’ ’/’ direkt vor %%, hat der Parser keine Konflikte mehr und parst arithmetische Ausdrücke entsprechend der üblichen geltenden Konventionen (Punkt vor Strich usw.). 3.2 Objektorientierung in Python Im Mittelpunkt objektorientierter Programmierung stehen Objekte, hierbei wird die Ansicht vertreten, dass jedes Element der realen oder ideellen Welt durch ein Objekt modelliert werden kann. Die Objekte haben eine Objektidentität, d.h. zwei inhaltlich gleiche Objekte sind voneinander unterscheidbar. Zudem sind Objekte zustandsbehaftet. Meist wird dieser Zustand durch die Belegung der objekteigenen Variablen (Attribute) festgelegt. Desweiteren besitzen Objekte Methoden, die den Zustand des Objekts verändern können oder Nachrichten mit anderen Objekten austauschen können. Innerhalb der Objekte (Variablen, Methoden) findet oft der imperative Programmierstil Anwendung, weswegen die objektorientierte Programmierung auch lediglich als Strukturierungsmittel für imperative Programme angesehen werden kann. Die meisten objektorientierten Programmiersprachen stellen Hilfsmittel zur Erzeugung und Verwaltung von Objekten bereit: Klassen dienen als Muster für neu zu erzeugende Objekte, Objekte gleicher Klassen haben gleiche Attribute und Methoden. Durch das Mittel der Vererbung können Unterklassen von bestehenden Klassen abgeleitet werden, die so geschaffenen Klassen erben die Attribute und Methoden der Oberklasse, und können diese abändern (überschreiben) oder weitere Attribute und Methoden hinzufügen. Python unterstützt Klassen mit Vererbungsmechanismus, es fehlen jedoch private Methoden und Attribute, d.h. alle Methoden und Attribute sind von außen her sicht-, aufrufund modifizierbar. 3.2.1 Syntax der Klassen-Definition Die einfachste Klassen-Definition ist von der Form: 27 class Klassenname: <Anweisungen> Normalerweise werden die Anweisungen innerhalb einer Klassendefinition Funktionsdefinitionen und Zuweisungen sein. Wenn eine Klassen-Definition ausgeführt wird, wird ein neuer Namensraum erzeugt und als lokaler Geltungsbereich verwendet. D.h. insbesondere, dass alle Zuweisungen an lokale Variablen in diesem Namensraum geschehen, definierte Funktionen sind außerhalb des Namensraums über Klassenname.Funktionsname aufrufbar. Ein Beispiel für eine Klasse zur Repräsentation von Rechtecken: class Rechteck: seite_horizontal = 0 seite_vertikal = 0 def flaechen_inhalt(self): return self.seite_horizontal * self.seite_vertikal def set_seite_horizontal(self, sh): self.seite_horizontal = sh def set_seite_vertikal(self, sv): self.seite_vertikal = sv Diese Klasse hat die Attribute (lokalen Variablen) Rechteck.seite_horizontal und Rechteck.seite_vertikal und definiert die Funktionen Rechteck.flaechen_inhalt, Rechteck.set_seite_horizontal und Rechteck.set_seite_vertikal. 3.2.2 Objekte – Instanzen von Klassen Die Syntax zur Erzeugung eines Objektes in Python lautet: Klassenname(parameter) Die Ausführung eines solchen Befehls erzeugt ein Objekt der Klasse Klassename, ein Rechteck, dessen Seitenlängen 4 und 6 betragen, kann wie folgt erzeugt werden: einRechteck = Rechteck() Rechteck.set_seite_horizontal(einRechteck,4) Rechteck.set_seite_vertikal(einRechteck,6) Als abkürzende Schreibweise gilt für Objekte, dass sie die Funktionen als Methoden selbst zur Verfügung stellen, d.h. das Setzen der Länge der horizontalen Seite ist auch möglich über: einRechteck.set_seite_horizontal(4) 28 Man beachte, dass wir nun die Methode set_seite_horizontal des Objektes einRechteck aufgerufen haben im Gegensatz zur Funktion Rechteck.set_seite_horizontal. Der Unterschied zwischen Methode und Funktion ist, dass bei der Methode stets das Objekt selbst als erster Parameter übergeben wird, d.h. die Aufrufe Rechteck.set_seite_horizontal(einRechteck,4) und einRechteck.set_seite_horizontal(4) sind gleich. Bisher ist es ziemlich mühsam, ein Objekt für ein spezielles Rechteck zu erzeugen: Wir müssen ein Objekt erzeugen und danach die Werte für die horizontale und vertikale Seite setzen. Besser wäre es direkt beim Erzeugen die entsprechenden Werte mitanzugeben. Dafür stellt Python die Funktion __init__ zur Verfügung, wird diese innerhalb einer Klassendefinition definiert, so wird __init__ bei jedem Erzeugen eines Objektes ausgeführt, wobei der erste Parameter von __init__ stets das Objekt selbst ist. D.h. wir können die Klasse Rechteck definieren als: class Rechteck: seite_horizontal = 0 seite_vertikal = 0 def __init__(self,sh,sv): self.seite_horizontal = sh self.seite_vertikal = sv ... und unser Objekt einRechteck durch die Anweisung einRechteck = Rechteck(4,6) erzeugen. 3.2.3 Private Attribute Python bietet keine echten Mechanismen, um Attribute einer Klasse vor dem Zugriff von ” außen“ zu schützen. Wir könnten jederzeit die Seitenlängen unseres Rechtecks mittels einRechteck.seite_horizontal abfragen oder diesen neue Werte zuweisen, auch wenn dies eher schlechtem Programmierstil entspricht. Normalerweise sollten Attributwerte nur über Objektmethoden manipuliert werden. Einen kleinen Schutz bietet Python dafür dann doch: Attribute, deren Namen mit zwei Unterstrichen (__) beginnen, sind mit diesem Namen nicht sichtbar von außen, der Name wird um den Prefix _<Klassenname> erweitert. Ein Beispiel hierzu class Rechteck: __seite_horizontal = 0 ... 29 >>> einRechteck = Rechteck(4,6) >>> einRechteck.__seite_horizontal Traceback (most recent call last): File "<stdin>", line 1, in ? AttributeError: Rechteck instance has no attribute ’__seite_horizontal’ >>> einRechteck._Rechteck__seite_horizontal 4 3.2.4 Vererbung Klassen können von anderen Klassen abgeleitet sein, die Syntax hierfür ist: class NeueKlasse(Basisklasse): <Anweisungen> Objekte abgeleiteter Klassen erben die Methoden und Attribute der Basisklasse, d.h. diese sind für das Objekt vorhanden. Die Klassendefinition der abgeleiteten Klasse darf diese Methode überschreiben, d.h. durch eine neue Definition ersetzen. Außerdem ist es natürlich möglich, neue Funktionen für die abgeleitete Klasse zu definieren. Ein Beispiel: Wir definieren eine Klasse Quadrat, die von der Klasse Rechteck abgeleitet ist, und überschreiben die __init__-Methode class Quadrat(Rechteck): def __init__(self,seitenlaenge): self.seite_horizontal = seitenlaenge self.seite_vertikal = seitenlaenge Python unterstützt noch weitere Mechanismen der Objektorientierung, wie z.B. Mehrfachvererbung. Für tiefergehende Grundlagen bzgl. Objektorientierung und Python sei auf die Literatur verwiesen (z.B. Kapitel 7 – 9 aus [2] sowie Kapitel 9 aus [10]) 3.3 3.3.1 Tkinter Was ist Tkinter? Tkinter ist eine Python-Bibliothek, die es ermöglicht recht einfach graphische Benutzeroberflächen zu erstellen. Tkinter bietet eine Python-Schnittstelle zu Tk, dem GUI Toolkit für Tcl/Tk. Tcl (Tool Command Language) ist eine einfache, vollständig interpretierte Skript-Sprache. Durch die modulare Architektur bietet sich Tcl hervorragend für Erweiterungen an. Tk 30 (Toolkit) ist eine solche Erweiterung von Tcl, um die Fähigkeit zur Darstellung von Elementen Graphischer Benutzeroberflächen, wie Fenster, Menüs, Buttons usw. Die Darstellung der Elemente erfolgt dabei im nativen Design der jeweiligen Oberfläche. Der große Vorteil der Verwendung von Tcl/Tk ist dessen weite Verbreitung und Verfügbarkeit für zahlreiche Plattformen. 3.3.2 Ein Beispiel Das folgende Beispiel soll einen Eindruck von der Programmierung mit Tkinter geben. Für die Bearbeitung der entsprechenden Praktikumsaufgabe wird dieser Abschnitt nicht als alleinige Informationsquelle ausreichen. Es sei insbesondere auf [7], [3] sowie Kapitel 10 & 11 aus [2] verwiesen, die allesamt, die verfügbaren Widgets ausführlich darstellen und deren Verwendung erläutern. Das einfache Beispiel: 1 from Tkinter import * 2 3 class App: 4 def __init__(self): 5 root = Tk() 6 self.meinKnopf = Button(root, text="Klick mich", command=self.setLabel ) 7 self.meinLabel = Label(root,text="") 8 self.meinKnopf.pack(side=TOP) 9 self.meinLabel.pack(side=BOTTOM) 10 mainloop() 11 12 def setLabel(self): 13 self.meinLabel.config(text="Hallo!") 14 15 App() Erläuterungen: Zeile 1 Es wird das Modul Tkinter importiert Zeile 3-13 Es wird eine Klasse App für die Applikation definiert. Zeile 15 Es wird ein Objekt der Klasse App erzeugt. Die Klasse App: Zeile 4-10 Es wird die Klassenmethode __init__ definiert, diese wird bei jedem Erzeugen eines Objektes der Klasse ausgeführt, als Parameter erhält sie das erzeugte Objekt. Zeile 5 Es wird das Tkinter-Hauptfenster initialisiert und an den Namen root gebunden. 31 Zeile 6 Es wird das Attribut meinKnopf erzeugt, welches ein Objekt der Klasse Button ist, die übergebenen Parameter bei der Instanziierung bedeuten: Der erste Parameter ist der Container“, der das Widget enthalten soll, in diesem Fall root also ” das Hauptfenster. Der Wert für das Attribut text ist "Klick mich!", das ist die Aufschrift des Knopfes. Schließlich gibt command an, welche Funktion ausgeführt werden soll, falls der Knopf gedrückt wird. Diese ist die Objektmethode setLabel Zeile 7 Es wird eine Beschriftung (Label) erzeugt, die ebenfalls im Container root erscheinen soll und zunächst keinen Text anzeigt. Zeile 8-9 Die Widgets erscheinen erst dann, wenn sie mit einer pack-Methode gepackt wurden, möglich sind z.B. pack oder grid. Hier werden die beiden Widgets mit pack gepackt, wobei der Knopf oben, der Label unten angeordnet erscheinen sollen. Zeile 10 Es wird die Funktion mainloop aufgerufen, die dafür sorgt, dass sämtliche Ereignisse (Tastatureingaben, Mausbewegungen, Drücken von Knöpfen der Maus) an das Hauptfenster weitergeleitet werden. Zeile 12-13 Es wird die Methode setLabel definiert, die die Konfiguration des Labels meinLabel derart ändert, dass der Text den Wert "Hallo" bekommt. Das Programm erzeugt dann ein Fenster mit Button und Label, wobei die Aufschrift nach dem Klicken des Buttons geändert wird. 3.4 3.4.1 Netzwerkprogrammierung mit Sockets Ein Client-Server-Netzwerk In einem Client-Server-Netzwerk gibt es zwei unterschiedliche Arten von Systemen, i.a. • mehrere Clients • einen Server Der Server stellt einen Dienst zur Verfügung. Die Clients beanspruchen den Dienst des Servers, indem sie Anfragen an den Server stellen. Während die Clients nur dann Kontakt zum Server aufnehmen, wenn sie eine Anfrage stellen wollen, wartet der Server ständig auf Anfragen von den Clients. 32 3.4.2 Was ist ein Socket? Ein Socket ist ein Endpunkt einer 2-Wege Kommunikationsverbindung zwischen zwei Programmen, die in einem Netzwerk laufen. Es gibt unterschiedliche Typen von Sockets, wir werden hier aber nur auf sogenannte Stream Sockets“ eingehen, die das TCP Protokoll benutzen. ” Das TCP (Transmission Control Protocol) stellt einen zuverlässigen Punkt-zu-Punkt Kommunikationskanal zur Verfügung. TCP wird von Client-Server-Anwendungen über das Internet benutzt. Um über TCP zu kommunizieren, bauen der Client und der Server eine Verbindung zueinander auf, indem beide Programme ein Socket erzeugen, und dieses dann wie einen Zeichenstrom (stream) behandeln, von dem gelesen bzw. auf den geschrieben werden kann. Genauer sind die Schritte im allgemeinen: Auf der Client-Seite: 1. Erzeuge einen Socket. 2. Verbinde den Socket mit der Adresse des Servers. Der Client muss hierfür den Namen bzw. die IP-Adresse und den Port des Servers kennen, mit dem er kommunizieren möchte. 3. Sende und empfange Daten. Auf der Server-Seite: 1. Erzeuge einen Socket. 2. Binde den Socket an eine Adresse. Hier ist die Adresse der Port des Servers, über den er mit den Clients kommunizieren möchte. Beachte: der Server benötigt keine Adressdaten der Clients. 3. Führe Listen“ auf dem Socket aus, normalerweise kann hier festgelegt werden, wie ” viele gleichzeitige Verbindungen der Server zulässt. 4. Akzeptiere die Verbindung eines Clients. Hier wird normalerweise solange gewartet, bis sich ein Client verbindet. 5. Sende und empfange Daten 3.4.3 Sockets in Python und Haskell Haskell stellt Schnittstellen zur Socket-Programmierung in der Standardbibliothek Network bereit, für die Programmierung sei auf die folgenden Quellen verwiesen: • Die Dokumentation der Network-Bibliothek ist online unter http://haskell.org/ghc/docs/6.2.1/html/libraries/network/Network.html abrufbar 33 • Es gibt eine Wiki-Seite von Ahn Ki-Yung zur Netzwerk-Programmierung in Haskell, diese ist über http://kyagrd.dyndns.org/wiki/HaskellServerProgramming zu finden, und wird unter http://www.ki.informatik.uni-frankfurt.de/doc/mirror/HSP/ gespiegelt. Für Python existiert das Modul socket, welches in der Standarddistribution enthalten ist und Schnittstellen zur Socket-Programmierung bereit stellt. Quellen zur SocketProgrammierung in Python sind zahlreich im Internet zu finden, hier seien einige genannt: • Eine Kurzeinführung gibt der Artikel http://www.devshed.com/c/a/Python/Socketsin-Python/ • Sehr empfehlenswert ist das Socket Programming HOWTO“ von Gordon McMil” lian, welches über http://www.amk.ca/python/howto/sockets/ abrufbar ist. • Kapitel 20 von [2] gibt eine kurze Einführung in die von uns eingesetzten Stream” Sockets“ und geht dann darüber hinaus und erklärt Datagram Sockets“ ” • Die Python Library Reference bietet die Dokumentation zum Modul socket, welche online über http://www.python.org/doc/2.3.5/lib/module-socket.html abrufbar ist. 34 Literaturverzeichnis [1] Manuel M. T. Chakravarty and Gabriele C. Keller. Einführung in die Programmierung mit Haskell. Pearson Studium, 2004. [2] H.M. Deitel, P. J. Deitel, J. P. Liperi, and B. A. Wiedermann. Python How To Program. Prentice Hall, 2002. [3] John E. Grayson. Python and Tkinter Programming. Manning, 2000. [4] Daryl Harms. Using IDLE. ausschließlich http://www.python.org/idle/doc/idlemain.html. online verfügbar über [5] Paul Hudak, John Peterson, and Joseph H. Fasel. A gentle introduction to haskell, 2000. online verfügbar unter http://haskell.org/tutorial/. [6] Simon Peyton Jones, editor. Haskell 98 Language and Libraries. Cambridge University Press, April 2003. auch online verfügbar unter http://haskell.org/definition. [7] Fredrik Lundh. An Introduction to Tkinter. verfügbar über http://www.pythonware.com/library/tkinter/an-introduction-to-tkinter.pdf, 1999. [8] Simon Marlow and Andy Gill. Happy User Guide, 1997-2001. online verfügbar über http://haskell.org/happy/doc/html/index.html. [9] Simon Thompson. Haskell – The Craft of Functional Programming. Addison-Wesley, 1999. [10] G. van Rossum. Python tutorial, 2005. verfügbar über http://www.python.org/doc/2.3.5/tut/tut.html die deutsche Übersetzung ist in Version 1.5.2 verfügbar über http://starship.python.net/crew/gherman/publications/tutde/online/tut/. 35