Praktikumsanleitung - Professur für Künstliche Intelligenz und

Werbung
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
Herunterladen