wxHaskell: GUI-Programmierung mit Haskell

Werbung
wxHaskell: GUI-Programmierung mit Haskell
basierend auf "wxHaskell: A Portable and Concise GUI Library for Haskell"
von Daan Leijen
Eine Ausarbeitung von
Ramon Gudschun
Betreuung durch
Prof. Dr. Michael Hanus
1
Inhaltsverzeichnis
1 Einleitung..........................................................................................................................................3
2 Programmieren mit wxHaskell..........................................................................................................4
2.1 Erste Schritte..............................................................................................................................4
2.2 Menüs und Ereignisse................................................................................................................5
2.3 Dialoge.......................................................................................................................................7
2.3.1 Standarddialoge..................................................................................................................7
2.3.2 Anwendungsspezifische Dialoge.....................................................................................10
2.4 Layout......................................................................................................................................11
2.4.1 Basislayouts......................................................................................................................11
2.4.2 Ausrichtung......................................................................................................................12
2.4.3 Expansion.........................................................................................................................12
2.4.4 Ausdehnung......................................................................................................................13
2.4.5 Kombinationen.................................................................................................................13
2.4.6 Beispiel.............................................................................................................................14
3 Typsystem und Implementierung....................................................................................................17
3.1 Das Hierarchie-Problem..........................................................................................................18
3.2 Vererbung.................................................................................................................................18
3.3 Attribute und Eigenschaften.....................................................................................................19
3.3.1 Attribute wiederverwenden..............................................................................................20
3.3.2 Implementierung von Attributen......................................................................................20
3.4 Lösung des Font-Problems......................................................................................................21
3.5 Kommunikation mit C++.........................................................................................................23
4 Bewertung........................................................................................................................................23
2
1 Einleitung
"The ideal graphical user interface (GUI) library is efficient, portable across platforms, retains a
native look-and-feel, and provides a lot of standard functionality. [...] There is no intrinsic difficulty
in implementing a GUI library that provides the above features." (Daan Leijen, [1], S.1). Zu
deutsch: Die ideale Bibliothek für graphische Benutzeroberflächen ist effizient, auf verschiedene
Plattformen portierbar, erhält das native Look & Feel1 und stellt eine große Menge von
Standardfunktionen zur Verfügung. Es ist nicht besonders schwierig, eine solche Bibliothek zu
implementieren.
Nach Leijen ist das Problem vielmehr die riesige Menge an Arbeit, die ein solches Vorhaben macht.
Um diesen Arbeitsaufwand möglichst gering zu halten, liegt es also nahe, eine bestehende
Bibliothek bei der Entwicklung einer neuen zu nutzen.
wxWidgets[2] ist eine freie Bibliothek zur Programmierung einer graphischen Benutzerschnittstelle
(Graphical User Interface, GUI) in der Programmiersprache C++. Sie wird seit 1992 entwickelt und
hat eine große Community von Entwicklern. Sie wurde bereits für mehrere große Projekte in der
Industrie verwendet und wird von dieser unterstützt. Auszeichnend für wxWidgets ist die
Beibehaltung des nativen Look & Feel auf verschiedenen Plattformen, wie Windows, Mac OS X
und GTK (z.B. Linux) (Abbildung 1). Dazu stellt wxWidgets eine Schnittstelle zu den Widgets2
dieser Plattformen bereit.
Abbildung 1: wxWidgets unter Mac OS X, Windows und GTK+
Quelle: wxWidgets Homepage: http://www.wxwidgets.org/about/screensh.htm
wxWidgets wurde bereits in andere Programmiersprachen als C++ eingebettet, darunter Python
(wxPython), Perl (wxPerl) und Java. Hier soll nun vorgestellt werden, wie dies in Haskell gemacht
wurde bzw. wird und wie eine GUI mit Haskell entwickelt werden kann: wxHaskell[3].
wxHaskell besteht aus zwei Bibliotheken, den Paketen WXCore und WX. WXCore stellt die
Basisfunktionen von wxWidgets zur Verfügung, allerdings auf einem geringen Abstraktionsniveau.
Das sind etwa 2800 Methoden und über 500 Klassen von wxWidgets. Mit WXCore ist es bereits
möglich, vollständige GUIs zu entwickeln, die Programme sind dabei ihren C++-Pendants sehr
ähnlich.
Aufgesetzt auf WXCore befindet sich das Paket WX. Das Ziel von WX ist es, die Programmierung
zu erleichtern, indem es von der reinen wxWidgets-Schnittstelle abstrahiert. Hier kommen die
Stärken von Haskell zum Vorschein: es werden Überladung von Typklassen, Funktionen höherer
1 Look & Feel bezeichnet Aussehen und Handhabung einer graphischen Benutzerschnittstelle, wie z.B. Farben,
Layout und insbesondere die Benutzung und das Verhalten von graphischen Bedienelementen. Look & Feel
unterliegt auf verschiedenen Plattformen einer Standardisierung, die meist durch den jeweiligen Hersteller
vorgegeben wird.
2 Ein Widget ist ein kleines Programm, das in eine graphische Benutzerschnittstelle integriert wird und nicht
unabhängig funktioniert. Beispiele für Widgets sind einfache Kontrollelemente wie Knöpfe (Buttons) und
Textfelder, aber auch Dialoge und Fenster (Frames) sind Widgets.
3
Ordnung und Polymorphie verwendet. Außerdem enthält WX viele Kombinatoren, um das Layout
einer GUI zu gestalten.
2 Programmieren mit wxHaskell
In diesem Abschnitt soll anhand eines Beispiels die Programmierung einer GUI in wxHaskell
vorgestellt werden. Das Zielprogramm ist eine einfache Textverarbeitung, diese wird im folgenden
Schritt für Schritt erweitert. Alle Programme wurden mit wxHaskell-Version 0.11 unter Windows
XP getestet. Die Screenshots wurden ebenfalls unter Windows XP aufgenommen.
2.1 Erste Schritte
Zunächst wird ein Standardfenster (Frame) erzeugt, das ein Textfeld zur Eingabe von Zeichen
enthält.
module Main where
import Graphics.UI.WX
main = start wordProc
wordProc :: IO ()
wordProc =
do
f
<- frame [text := "Word Processor"]
currText
<- textCtrl f [text := "Hello World"]
set f [ layout
clientSize
:= fill $ (widget currText),
:= sz 800 600]
Nach der Deklaration des Moduls wird das Paket WX importiert, WXCore wird hier noch nicht
benötigt. In der main-Funktion wird dann mit Hilfe der start-Funktion das Hauptprogramm
initialisiert. Die start-Funktion registriert die Anwendung im graphischen Subsystem und startet
eine Ereignis-Schleife. Das einzige Argument von start ist ein IO-Wert (Funktion wordProc),
der bei der Initialisierung aktiviert wird. Dort wird die initiale Benutzerschnittstelle erzeugt und
weitere Event-Handler ("Ereignis-Behandler") definiert. Während die Ereignis-Schleife läuft, wird
Haskell nur über diese Event-Handler angesprochen.
In der Funktion wordProc wird ein Fenster erzeugt und an die Variable f gebunden.
Konstruktoren aus dem Paket WX, wie frame, erhalten eine Liste von Attributen als Parameter.
Sein Typ ist
frame :: [Prop (Frame ())] -> IO (Frame ())
Die Erweiterungen des Typsystems durch wxHaskell werden unter Abschnitt 3 genauer erklärt.
Ebenso der Unterschied zwischen den Datentypen Prop (=Property, also Eigenschaft) und Attr
(=Attribute). Zunächst kann beides mit "Eigenschaft" beschrieben werden. Dem Attribut text wird
hier die Zeichenkette "Word Processor" zugewiesen bzw. initialisiert (der Zuweisungsoperator ":="
ist ebenfalls ein Konstruktor, genaueres dazu folgt in Abschnitt 3) und damit der Text in der
Titelleiste gesetzt. Attribute, die nicht initialisiert werden, erhalten einen Standardwert. In der
nächsten Zeile wird eine TextCtrl erzeugt. Das ist ein mehrzeiliges Textfeld, in das vom
4
Benutzer Zeichen eingegeben werden können. Der Typ des zugehörigen Konstruktors textCtrl
lautet
textCtrl :: Window a -> [Prop (TextCtrl ())] -> IO (TextCtrl ())
Im Gegensatz zu Fenstern benötigen andere Widgets neben der Attributliste noch ein weiteres
Argument, das auf das Eltern-Widget (vom Typ Window a) verweist, in dem sie dargestellt
werden sollen. Hier ist es das Fenster f, das als Container dient. Attribute wie text können bei
verschiedenen Widgets auch unterschiedliche Bedeutungen haben. Bei einem Fenster wird darüber
der Text seiner Titelleiste bestimmt, bei einer TextCtrl der enthaltene Text, inklusive vom
Benutzer eingegebener Zeichen.
Neben der Bestimmung von Attributen bei der Erzeugung von Widgets ist es außerdem möglich,
ihnen mittels
set :: w -> [Prop w] -> IO ()
neue Werte zuzuweisen (IO () entspricht etwa void in imperativen Sprachen). Analog gelangt
man mit
get :: w -> Attr w a -> IO a
an den Wert eines Attributs.
In der nächsten Zeile werden die Attribute layout und clientSize von f gesetzt. Das Layout
wird über Kombinatoren definiert, denen Abschnitt 2.4 gewidmet ist. Hier wurde der fillKombinator gewählt, der bewirkt, dass die TextCtrl den gesamten verfügbaren Platz einnimmt.
widget :: Widget w => w -> Layout
überführt currText in den Typ Layout, der vom Attribut layout erwartet wird. Das Attribut
clientSize bestimmt schließlich die Größe des Fensters in Pixeln, wobei sz die angegebenen
Werte in das erwartete Format überführt (clientSize ist auch auf andere Widgets, wie z.B.
Buttons (Knöpfe) anwendbar). Das Ergebnis ist in Abbildung 2 zu sehen.
Abbildung 2: Erste Schritte
Abbildung 3: Menüs und
Ereignisse
2.2 Menüs und Ereignisse
Als nächstes wird ein Menü zum Programm hinzugefügt, über das es beendet oder ein neues
Dokument angelegt werden kann (Abbildung 3).
5
wordProc =
do
...
status
<- statusField [text := "A simple word processor"]
file
<- menuPane [text := "&File"]
new
<- menuItem file [
text := "&New\tCtrl+N",
help := "Create a new document"]
menuLine file
quit
<- menuQuit file [help := "Exit the program"]
set f [ ...
statusBar
:= [status],
menuBar
:= [file],
on (menu new)
:= onNew currText,
on (menu quit)
:= close f]
Als zusätzliche Erweiterung wird zunächst ein StatusField erzeugt. Am unteren Rand des
Fensters können damit zusätzliche Informationen angezeigt werden. Der bei der Initialisierung
definierte Text wird beim Start des Programms angezeigt. Mit menuPane wird dann ein Menü mit
dem Text "File" erzeugt. Das &-Zeichen führt dazu, dass dieses Menü automatisch ausgewählt
wird, wenn die Taste "F" auf der Tastatur gedrückt wird, während die Menüzeile aktiv ist (da hier
nur ein Menü definiert wird, ist diese Funktion wirkungslos). Mit menuItem wird anschließend
zum Menü file eine Schaltfläche mit dem Text "New" hinzugefügt. Wie zuvor führt hier das &Zeichen dazu, dass diese Schaltfläche über die Tastatur betätigt werden kann, wenn das Menü aktiv
ist. Durch den Zusatz "\tCtrl+N" kann dieser Menüpunkt außerdem direkt (ohne
Voraussetzungen) durch die Tastenkombination Ctrl+N ausgewählt werden. Über das Attribut help
kann der angegebene Hilfetext automatisch in einem StatusField angezeigt werden, wenn die
Schaltfläche aktiv ist. Nun wird mit menuLine eine horizontale Trennlinie in das Menü eingefügt.
Mit menuQuit wird, analog zur Vorgehensweise bei new, ein Menüpunkt zum Beenden des
Programms erzeugt. Der Text der Schaltfläche wird automatisch auf "Quit" gesetzt, mit den
passenden Tastenkombinationen.
Die statusBar eines Fensters ist eine Liste von StatusFields, die am unteren Rand des
Fensters einzeilig angezeigt werden. Das oben definierte StatusField wird hier hinzugefügt.
Weitere StatusFields würden hintereinander in der gleichen Zeile eingefügt werden. Über
help definierte Hilfetexte werden nur im ersten Feld angezeigt. Mit menuBar werden analog
einzelne Menüs in der Menüzeile am oberen Rand des Fensters eingefügt.
Event-Handler für Menüs werden über die Attribute des Fensters definiert, da Menüs direkt zum
Fenster gehören (deshalb muss bei menuPane auch kein Eltern-Widget angegeben werden). Die
Funktion
on :: Event w a -> Attr w a
liefert zu einem Ereignis (genauer: Event-Filter), das an einem Widget auftreten kann, ein
entsprechendes Attribut. Diesem Attribut wird dann der Event-Handler zugewiesen, hier die
Funktion onNew:
6
onNew currText =
do
set currText [text := ""]
repaint currText
Wird der Menüpunkt new betätigt, werden durch onNew alle Zeichen in currText gelöscht. Um
die Änderungen anzeigen zu lassen, muss danach das Widget mittels repaint neu gezeichnet
werden.
Weitere Beispiele von Ereignissen (f sei ein Frame):
●
Bei Buttons wird der Event-Handler über on command zugewiesen.
b <- button f []
set b [on command := <Event-Handler>]
●
Maus-Ereignisse:
set f [on motion := handlePosition]
Es wird ein Event-Handler erwartet, der wenigstens ein Argument besitzt, das einen Punkt,
das heißt den Ort des Ereignisses, aufnehmen kann.
handlePosition :: Point2 a -> IO ()
●
Tastatur-Ereignisse:
set f [on (charKey '+') := <Event-Handler>]
2.3 Dialoge
2.3.1 Standarddialoge
Eine Textverarbeitung, bei der das Geschriebene beim Schließen des Programms verschwindet, ist
wenig sinnvoll. Deshalb werden nun Funktionen zum Öffnen und Speichern von Textdateien mit
Hilfe von Dialogen hinzugefügt. Die am häufigsten benötigten Standarddialoge sind in wxHaskell
bereits vordefiniert.
Zuerst werden Dateiformate für Textdateien spezifiziert, die in der Anwendung zugelassen werden
sollen. Diese werden den Dialogen später übergeben.
textFiles = [("TXT files (*.txt)",["*.txt"])]
Die vordefinierten Dialoge erwarten diese Spezifikation (Wildcards) in Form einer Liste von
Tupeln. Jedes dieser Tupel enthält eine Beschreibung des Dateiformats und eine Liste der
möglichen Suffixe. Um die Anwendung einfach zu halten, sind hier nur txt-Dateien erlaubt, die
Liste der Suffixe enthält somit nur "*.txt".
Analog zu Abschnitt 2.2 wird die Hauptfunktion außerdem um weitere Menüpunkte zum Öffnen
und Speichern von Dateien erweitert, sowie geeignete Event-Handler. Zu beachten ist hier lediglich,
dass die Menüelemente in der gleichen Reihenfolge, wie sie deklariert werden, auch in das Menü
eingefügt werden.
7
wordProc =
do
...
currFile
<- variable [value := Nothing]
...
open
<- menuItem file [
text := "&Open\tCtrl+O",
help := "Open a document"]
save
<- menuItem file [
text := "&Save\tCtrl+S",
help := "Save the current document"]
saveAs
<- menuItem file [
text := "Save as",
help := "Save the current document"]
...
set f [ ...
on (menu open)
:= onOpen f currText currFile,
on (menu save)
:= onSave f currText currFile,
on (menu saveAs)
:= onSaveAs f currText currFile]
Neu ist hier die Deklaration einer Variablen durch den Konstruktor
variable :: [Prop (Var a)] -> IO (Var a)
In der Anwendung enthält currFile den Pfad zur aktuell geöffneten Datei oder Nothing, wenn
keine Datei geöffnet ist. Das einzige Attribut einer Variablen ist ihr Wert value, eines beliebigen
Typs.
Entsprechend muss der Event-Handler onNew angepasst werden, so dass der Wert der Variablen auf
Nothing gesetzt wird.
onNew currText currFile =
do
set currText [text := ""]
set currFile [value := Nothing]
repaint currText
Eine zusätzliche Erweiterung könnte, falls eine Datei geöffnet ist, vom Benutzer abfragen, ob diese
zuerst gespeichert werden soll.
Nun wird der Event-Handler zum Öffnen von Dateien definiert.
onOpen f currText currFile =
do
file <- fileOpenDialog f True False "Open file" textFiles "" ""
case file of
Nothing
-> return ()
Just fname
-> openTxtFile currText currFile fname
Mit fileOpenDialog wird ein (modaler) Standarddialog zum Öffnen von Dateien angezeigt.
Die Argumente dieser Funktion haben folgende Typen:
8
fileOpenDialog ::
Window a
Das Eltern-Widget, in dem der Dialog angezeigt werden soll.
-> Bool
Soll beim nächsten Start automatisch zum zuletzt gewählten Verzeichnis gesprungen werden?
-> Bool
Sollen auch Dateien ausgewählt werden können, die nur gelesen werden dürfen?
-> String
Der Titel des Dialogs.
-> [(String, [String])]
Die Einträge in der Auswahlbox für Dateiformate (Wildcards), wie oben in textFiles definiert.
-> FilePath
Das Verzeichnis, das beim Anzeigen des Dialogs geöffnet werden soll. "" ist das aktuelle
Verzeichnis.
-> FilePath
Eine Datei, die beim Anzeigen des Dialogs ausgewählt werden soll. Bei "" wird keine ausgewählt.
-> IO (Maybe FilePath)
Die Rückgabe der Funktion ist der Pfad zur ausgewählten Datei (Just FilePath) oder
Nothing, wenn der "Cancel"-Button gedrückt wurde.
Falls eine Datei ausgewählt wurde, wird diese mit openTextFile geöffnet:
openTxtFile currText currFile fname =
do
content <- readFile fname
set currFile [value := Just fname]
set currText [text := content]
repaint currText
Durch die Verwendung des Dialogs ist sichergestellt, dass die Datei auch existiert. Mit der HaskellFunktion readFile (aus Prelude) wird der Inhalt der Datei als String eingelesen, in das Textfeld
eingefügt und die Variable currFile auf den Pfad der Datei gesetzt.
Ganz analog wird das Speichern von Dateien implementiert:
9
onSaveAs f currText currFile =
do
file <- fileSaveDialog f True True "Save file as..." textFiles "" ""
case file of
Nothing
-> return ()
Just fname
-> saveTxtFileAs currText currFile fname
saveTxtFileAs currText currFile fname =
do
content <- get currText text
writeFile fname content
set currFile [value := Just fname]
Das dritte Argument von fileSaveDialog gibt an, dass der Benutzer gefragt wird, ob eine Datei
überschrieben werden soll. Die übrigen Argumente sind wie bei fileOpenDialog.
Schließlich gibt es noch eine Funktion zum direkten Speichern der aktuell geöffneten Datei. Ist
keine geöffnet, wird onSaveAs aufgerufen:
onSave f currText currFile =
do
content <- get currText text
file
<- get currFile value
case file of
Nothing
-> onSaveAs f currText currFile
Just fname
-> writeFile fname content
Ein Dialog zum Öffnen von Dateien ist in Abbildung 4 zu sehen.
Abbildung 4: Standarddialog zum Öffnen einer Datei
2.3.2 Anwendungsspezifische Dialoge
Neben den Standarddialogen werden dennoch oft selbst definierte Dialoge benötigt. Nicht-modale
Dialoge können mit
10
dialog :: Window a -> [Prop (Dialog ())] -> IO (Dialog ())
erzeugt und über das Attribut visible sichtbar bzw. unsichtbar gemacht werden. Sie werden im
wesentlichen wie Frames benutzt (das erste Argument ist wie üblich ein Eltern-Widget, meistens ein
Frame).
Um einen modalen Dialog zu erzeugen, der die Ausführung des aufrufenden Programms blockiert,
solange er angezeigt wird, wird die Funktion
showModal :: Dialog b -> ((Maybe a -> IO ()) -> IO ()) -> IO (Maybe a)
verwendet. Neben einem Dialog, erwartet die Funktion eine weitere Funktion φ, die wiederum eine
Funktion ψ zum Argument hat. Ein modaler Dialog hat eine eigene Ereignisschleife, diese wird
durch φ definiert. Wird innerhalb der Schleife ψ aufgerufen, wird sie beendet und die Rückgabe von
showModal ist das Argument mit dem ψ aufgerufen wurde. Der folgende Codeausschnitt soll die
Benutzung verdeutlichen, setclose entspricht φ und close entspricht ψ:
myDialog :: Window a -> IO (Maybe String)
myDialog parent =
do
d
<- dialog parent [text := "Title"]
ok
<- button d [text := "Ok"]
can
<- button d [text := "Cancel"]
...
let setclose close = do set ok [on command := do str <- onOk; close str]
set can [on command := close Nothing]
result
<- showModal d setclose
return result
onOk :: IO String
2.4 Layout
Die bisherige Anwendung besteht im wesentlichen aus einer Textkomponente. Sollen weitere
Widgets hinzugefügt werden, müssen Parameter wie die Position eines Widgets innerhalb der
Anzeigefläche und ihre Größe bestimmt werden - ihr Layout. Oft ist außerdem eine dynamische
Anpassung des Layouts gewünscht, wenn sich das übergeordnete Layout ändert. In wxHaskell
werden diese Parameter über verschiedene Layout-Kombinatoren festgelegt, die auf ein Layout
angewendet werden können.
2.4.1 Basislayouts
Um aus einem Widget (das von Window abgeleitet ist) ein Layout zu erzeugen, können folgende
Kombinatoren verwendet werden:
widget ::
Window a -> Layout
container :: Window a -> Layout -> Layout
Die Funktion container wird für Widgets verwendet, die selbst Widgets enthalten, z.B.
scrollbare Fenster.
11
Einige Beispiele für einfache Basislayouts:
●
Eine statische Beschriftung.
label :: String -> Layout
●
Eine leere Fläche der angegebenen Länge und Breite.
space :: Int -> Int -> Layout
●
Eine schwarze Fläche der angegebenen Länge und Breite.
rule :: Int -> Int -> Layout
●
Ein Rahmen um ein Layout, mit Beschriftung.
boxed :: String -> Layout -> Layout
Auf die erhaltenen Layouts können nun die nachfolgenden Kombinatoren angewendet werden.
Voraussetzung für die ersten beiden Kategorien ist, dass die verfügbare Anzeigefläche größer als
das Layout ist.
2.4.2 Ausrichtung
Die Ausrichtung (alignment) eines Layouts legt seine Position innerhalb der Anzeigefläche fest.
wxWidgets lässt nur die Ausrichtung zu den Seiten oder in das Zentrum der Fläche zu (nicht
punktgenau), deshalb ist dies auch in wxHaskell der Fall. Die folgenden Primitive sind verfügbar:
halignLeft :: Layout -> Layout
halignRight :: Layout -> Layout
halignCenter :: Layout -> Layout
Horizontale Ausrichtung nach links, rechts oder in die Mitte. Die Standardeinstellung ist
halignLeft.
valignTop :: Layout -> Layout
valignBottom :: Layout -> Layout
valignCenter :: Layout -> Layout
Vertikale Ausrichtung nach oben, unten oder in die Mitte. Die Standardeinstellung ist valignTop.
2.4.3 Expansion
Über die Expansion eines Layouts wird bestimmt, wie bzw. ob das Layout vergrößert werden soll.
Das Layout ist entweder
rigid :: Layout -> Layout
und verändert seine Größe nicht, dies ist die Standardeinstellung oder
expand :: Layout -> Layout
und wird auf die Anzeigefläche vergrößert oder
shaped :: Layout -> Layout
und wird wie bei expand vergrößert, bei Beibehaltung des Seitenverhältnisses.
Expansion in nur eine Richtung, also horizontal oder vertikal, ist nicht möglich, da wxWidgets dies
nicht zulässt.
12
Abbildung 5: Expansion
2.4.4 Ausdehnung
Im Gegensatz zu Ausrichtung und Expansion spielt die Ausdehnung (stretch) eines Layouts eine
Rolle, wenn dem Layout eine größere Anzeigefläche als die minimal benötigte zugeordnet werden
soll - ohne das Layout selbst zu vergrößern. Dazu gibt es folgende Kombinatoren:
static :: Layout -> Layout
hstretch :: Layout -> Layout
vstretch :: Layout -> Layout
Bei der Standardeinstellung static ändert sich die Größe der Anzeigefläche nicht. Bei
hstretch wird sie horizontal und bei vstretch vertikal vergrößert. Horizontale und vertikale
Ausdehnung lassen sich auch wie folgt kombinieren, um die Anzeigefläche in beide Richtungen zu
vergrößern:
stretch = hstretch . vstretch
Damit sichergestellt ist, dass der insgesamt verfügbare Platz der Anzeigefläche genutzt werden
kann, wird auf das oberste Layout immer stretch angewendet. Ausdehnung ist insbesondere in
Kombination mit Ausrichtung sinnvoll.
Abbildung 6: Ausdehnung
2.4.5 Kombinationen
Einige Kombinationen der obigen Primitive werden durch wxHaskell bereitgestellt, z.B.:
●
alignCenter = halignCenter . valignCenter
alignBottomRight = halignRight . valignBottom
●
Bei Änderung der Größe des übergeordneten Layouts "fließt" ein Layout in die angegebene
Richtung der Anzeigefläche.
floatCenter = stretch . alignCenter
floatBottomRight = stretch . alignBottomRight
13
●
Ausfüllen der Anzeigefläche in der angegebenen Richtung.
hfill = hstretch . expand
vfill = vstretch . expand
fill = hfill . vfill
Um eine größere Anzahl von Layouts einfach verwalten zu können, sind mächtigere Kombinatoren
verfügbar:
row :: Int -> [Layout] -> Layout
column :: Int -> [Layout] -> Layout
row stellt eine Liste von Layouts in einer Zeile dar, das erste Argument gibt ihren Abstand in Pixeln
zueinander an. Analog stellt column eine Liste von Layouts in einer Spalte dar.
grid :: Int -> Int -> [[Layout]] -> Layout
ist eine Kombination von row und column. Das Ergebnis ist ein Layout mit einer TabellenStruktur. Es werden der Abstand zwischen den Elementen einer Zeile, der Abstand zwischen den
Elementen einer Spalte und eine Liste der Zeilen übergeben. Eine Zeile ist dabei eine Liste von
Layouts.
2.4.6 Beispiel
Die Textverarbeitung soll nun um eine Zoom-Funktion erweitert werden. Durch die Betätigung von
Buttons wird dabei die Font-Größe des gesamten Textes vergrößert oder verkleinert. Die
Schrittweite kann über einen Spinner geändert werden.
14
import Graphics.UI.WXCore
...
wordProc =
do
...
currText
<- textCtrlRich f [font := fontDefault]
...
zoomIn
<- bitmapButton f [
picture := "./zoomTextIn.png",
clientSize := sz 19 19,
tooltip := "Increase the
font-size"]
zoomOut
<- bitmapButton f [
picture := "./zoomTextOut.png",
clientSize := sz 19 19,
tooltip := "Decrease the
font-size"]
fontSizeSpinner <- spinCtrl f 1 50 [
clientSize := sz 40 19,
tooltip := "Adjust the font-size
stepping"]
...
set f [ layout := column 0 [
hstretch $ halignCenter $
row 1 [
widget zoomIn,
widget fontSizeSpinner,
widget zoomOut],
fill $ widget currText],
...]
set zoomIn [on command :=
textCtrlChangeFontSize currText fontSizeSpinner (+)]
set zoomOut [on command :=
textCtrlChangeFontSize currText fontSizeSpinner (-)]
Fonts und insbesondere ihre Größe können bei Verwendung von textCtrl nicht verändert
werden. Deshalb muss stattdessen textCtrlRich verwendet werden. Außerdem wird font
explizit initialisiert, da andernfalls textCtrlGetDefaultStyle (siehe unten) eine Font-Größe
von 0 liefert.
Zum Vergrößern und Verkleinern werden BitmapButtons verwendet, die anstelle von Text ein
Bild anzeigen. Ihre Größe wird etwas größer als die des Bildes gesetzt, so dass Umriss und Schatten
des Buttons erhalten bleiben. Der mit dem Attribut tooltip definierte Text, wird nach kurzer
Verzögerung angezeigt, wenn der Maus-Zeiger über den Button bewegt wird.
Mit
spinCtrl :: Window a -> Int -> Int -> [Prop (SpinCtrl ())]
-> IO (SpinCtrl ())
wird der erwähnte Spinner zum Einstellen der Schrittweite konstruiert. Das zweite und dritte
15
Argument spezifizieren den minimal und maximal einstellbaren Wert.
Das Layout des Frames gestaltet sich wie folgt:
Buttons und Spinner werden in einer Zeile (row) dargestellt, mit einem Abstand von einem Pixel.
Durch halignCenter sind sie mittig ausgerichtet und durch hstretch wird dazu die gesamte
Anzeigefläche in der Horizontalen genutzt. Mit Hilfe von column wird darunter das Textfeld
angezeigt, das durch fill den gesamten verbleibenden Platz einnimmt.
Die Buttons werden mit Event-Handlern versehen, die den Font vergrößern bzw. verkleinern, indem
jeweils die Addition oder die Subtraktion übergeben wird.
Intuitiv würde in diesen Event-Handlern nun die Font-Größe etwa mit
size <- get currText fontSize
<berechne newSize>
set currText [fontSize := newSize]
neu gesetzt werden. Die Attribute fontSize und font existieren zwar für eine TextCtrl,
verschiedenste Tests führten jedoch zu einem Absturz des GHC zur Laufzeit (durch get ...).
Wie eingangs erwähnt, abstrahiert das Paket WX lediglich von der wxWidgets-Schnittstelle. Wenn
für eine Aufgabe keine geeignete Abstraktion vorhanden ist, können Funktionen aus dem Paket
WXCore verwendet werden. Damit lässt sich auch das "Font-Problem" lösen.
Aber auch im Paket WXCore gibt es keine direkte Funktion, um an die Font-Größe oder den Font
zu gelangen. Zunächst werden deshalb Funktionen definiert, die dies ermöglichen:
textCtrlGetFont ctrl =
do
attr
<- textCtrlGetDefaultStyle ctrl
font
<- textAttrGetFont attr
return font
textCtrlSetFont ctrl font =
do
attr
<- textCtrlGetDefaultStyle ctrl
end
<- textCtrlGetLastPosition ctrl
textAttrSetFont attr font
textCtrlSetStyle ctrl 0 end attr
return ()
Eine TextCtrl ist mit einem Textstil-Attribut versehen, das mit
textCtrlGetDefaultStyle :: TextCtrl a -> IO (TextAttr ())
geholt wird. Aus diesem Attribut kann dann der Font extrahiert werden.
In der Funktion textCtrlSetFont wird dieses Stil-Attribut mit
textCtrlSetStyle :: TextCtrl a -> Int -> Int -> TextAttr d -> IO Bool
gesetzt. Das zweite und dritte Argument geben Anfangsposition und Endposition der zu ändernden
Zeichen an. Da die Font-Größe des gesamten Textes geändert werden soll, werden hier 0 als
Anfangsposition und die Position des letzten Zeichens als Endposition übergeben. Die Rückgabe
von textCtrlSetStyle wird ignoriert.
16
Die Größe eines Fonts kann nun mit fontGetPointSize ermittelt oder mit
fontSetPointSize gesetzt werden:
textCtrlGetFontSize ctrl =
do
font
<- textCtrlGetFont ctrl
fontSize
<- fontGetPointSize font
return fontSize
textCtrlSetFontSize ctrl size =
do
font
<- textCtrlGetFont ctrl
fontSetPointSize font size
textCtrlSetFont ctrl font
Schließlich kann nun der Event-Handler definiert werden:
textCtrlChangeFontSize :: TextCtrl a -> SpinCtrl a -> (Int -> Int -> Int)
-> IO ()
textCtrlChangeFontSize currText fontSizeSpinner fun =
do
fontSize
<- textCtrlGetFontSize currText
sizeStepping
<- get fontSizeSpinner selection
textCtrlSetFontSize currText (fun fontSize sizeStepping)
Die aktuelle Font-Größe, sowie die am Spinner eingestellte Schrittweite werden geholt. Dann wird
die an den Handler übergebene Funktionen auf diese Werte angewendet und die neue Font-Größe
gesetzt.
Das Endergebnis ist in Abbildung 7 zu sehen.
Abbildung 7: Beispiel
3 Typsystem und Implementierung
Wenn zum Beispiel eigene Widgets definiert wurden, ist es wünschenswert, auf diese nach dem
Vorbild des Paketes WX zugreifen zu können. Dazu ist ein gewisses Verständnis der Erweiterungen
des Typsystems durch wxHaskell erforderlich. Dieses Wissen wird auch eine Übergangslösung für
das "Font-Problem" (vgl. Abschnitt 2.4.6) liefern. Außerdem wird die Implementierung von
17
wxHaskell bzw. der Zugriff auf wxWidgets näher betrachtet.
3.1 Das Hierarchie-Problem
wxHaskell legt ein strenges Typsystem auf die wxWidgets-Bibliothek. Damit werden illegale
Operationen auf Widgets zurückgewiesen. Außerdem ist das Speichermanagement voll automatisch,
wobei es dem Benutzer ermöglicht wird, externe Ressourcen wie Bitmaps, manuell zu handhaben.
Zudem werden NULL-Zeiger mit einer (Haskell-)Fehlermeldung quittiert, anstatt zu einer
Schutzverletzung (segmentation fault) zu führen.
Allerdings tritt in wxHaskell das "Hierarchie-Problem" auf, unter dem viele GUIs leiden:
Bei der Erzeugung von Widgets muss immer (abgesehen von Frames) ein Eltern-Widget mit
angegeben werden. Dies wird als Relikt von GUI-Bibliotheken angesehen, bei denen die
Speicherverwaltung explizit erfolgt. Der Vorteil dabei ist, dass Kind-Widgets automatisch beendet
werden können, wenn das Eltern-Widget beendet wird. Natürlicher wäre es aber, Widgets ohne
diese Argumente zu erzeugen.
Aber auch ohne dieses Argument gäbe es Probleme bei der Spezifikation von Layouts. Diese
können nicht durch das Typsystem abgefangen werden, sondern treten zur Laufzeit auf. Drei Arten
von Fehlern können auftreten:
●
Widgets vergessen.
●
Ein Widget mehrmals zum Layout hinzufügen (Duplikate).
f <- frame []
b <- button []
set f [layout := row 1 [widget b, widget b]]
●
Die Hierarchie-Ordnung verletzen.
f <- frame []
b1 <- button []
b2 <- button []
set b1 [layout := widget b2]
Eine Lösung des "Hierarchie-Problems" ist zum Beispiel die Verwendung eines linearen
Typsystems[4][5]. Alternative Lösungsansätze haben jedoch das Problem, dass die
Implementierung bei mehreren Komponenten schnell unübersichtlich wird und ein kleiner Fehler an
einer Stelle des Programms, zu einem Typfehler an einer anderen Stelle führt. Deshalb wurde bei
wxHaskell auf diese Lösungen (vorläufig) verzichtet.
3.2 Vererbung
Da wxHaskell auf einer objektorientierten Bibliothek basiert, muss die Vererbungsrelation zwischen
Widgets modelliert werden. Um diese Relation abzubilden, werden so genannte Phantomtypen[6]
[7] verwendet, die so wxHaskell zu seiner Typsicherheit verhelfen (Phantomtypen sind derzeit nur
mit dem GHC möglich, weshalb wxHaskell-Programme ausschließlich mit dem GHC erstellt
werden können). Widgets in wxHaskell sind Zeiger auf Objekte in wxWidgets:
type Object a = Ptr a
type wird verwendet, um Objekt-Zeiger von anderen Zeigern unterscheiden zu können. Das
Argument a ist hier ein Phantomtyp, das heißt diesem Argument wird nie ein Wert übergeben, da
Zeiger einfach Adressen sind.
18
Für jede C++-Klasse gibt es nun einen Phantom-Datentypen, z.B.:
data CWindow a
data CFrame a
data CControl a
data CButton a
Diese werden in Phantomtypen verwendet. Da keine Werte von Phantomtypen benötigt werden,
sind auch keine Konstruktoren für die Datentypen erforderlich. Mit Typsynonymen der folgenden
Form wird nun die Hierarchie abgebildet:
type Window a = Object (CWindow a)
type Frame a = Window (CFrame a)
type Control a = Window (CControl a)
type Button a = Control (CButton a)
Die entsprechende Baumdarstellung ist in Abbildung 8 zu sehen.
Abbildung 8: Vererbung
Diese Hierarchie definiert nun wxHaskells Erweiterungen am Typsystem, die illegale Operationen
auf den Objektzeigern verhindern. Zur Erzeugung von Widgets dienen dann beispielsweise folgende
Funktionen:
frame
:: [Prop (Frame ())] -> IO (Frame ())
button :: Window a -> [Prop (Button ())] -> IO (Button ())
label
:: Window a -> [Prop (Label ())] -> IO (Label ())
Ein Typ C () erwartet dabei ein Objekt von genau der Klasse C, ein Typ C a ein Objekt von
wenigstens Klasse C (gleicher Ast der Vererbungshierarchie). Zum Beispiel erzeugt button ein
Objekt von genau der Klasse Button, kann aber als Eltern-Widget beispielsweise einen Frame
haben, da sich der Typ von Frame zu Window a auflösen lässt.
3.3 Attribute und Eigenschaften
Attribute (attributes) und Eigenschaften (properties) eines Widgets werden in wxHaskell
voneinander unterschieden. Attribute definieren, welche Eigenschaften ein Widget haben kann,
Properties dagegen, welche Eigenschaften ein Widget hat. Genauer: Properties sind Attribute,
denen Werte zugewiesen wurden.
Ein Attribut Attr w a kann auf Objekte vom Typ w angewendet werden und einen Wert des Typs
a aufnehmen. Das Attribut text eines Buttons hat zum Beispiel den Typ
19
text :: Attr (Button a) String
Der Wert eines Attributs kann mit
get :: w -> Attr w a -> IO a
extrahiert werden.
Um einem Attribut einen Wert zuzuweisen, wird der (:=)-Operator verwendet:
(:=) :: Attr w a -> a -> Prop w
Sein Ergebnis ist eine Property des Widgets w.
Schließlich wird einem Objekt mit
set :: w -> [Prop w] -> IO ()
eine Liste von Properties zugewiesen. Durch den Parameter w einer Property wird dabei weiterhin
sichergestellt, dass das Objekt nur Properties des passenden Typs erhält.
Analog zum (:=)-Operator wird mit dem (:~)-Operator
(:~) :: Attr w a -> (a -> a) -> Prop w
einem Attribut ein neuer Wert zugewiesen bzw. der alte durch die übergebene Funktion verändert.
Er stellt also eine Kombination von get und set dar und führt meist zu übersichtlicherem Code.
3.3.1 Attribute wiederverwenden
Aufgrund der Typhierarchie wäre es möglich, Attribute, wie z.B. text für beispielsweise jede Art
von Window zu definieren:
text :: Attr (Window a) String
Dies würde allerdings zu Problemen bei vom Benutzer definierten Widgets führen, bzw. diese
könnten das Attribut nicht benutzen. Stattdessen werden Attribute in Typklassen gekapselt:
class Textual w where
text :: Attr w String
instance Textual (Window a) where
text = ...
Dadurch steht das Attribut text jedem von Window abgeleiteten Objekt zur Verfügung.
Ein Nachteil bei diesem Ansatz ist, dass eine solche Instanzendeklaration im Haskell-98 Standard
nicht erlaubt ist. Werden die Typen expandiert
instance Textual (Ptr (CObject (CWindow a)))
ist die Deklaration nicht mehr von der erlaubten (flachen) Form (T a1 ... an). Beim GHC kann diese
Restriktion jedoch per Option aufgehoben werden.
3.3.2 Implementierung von Attributen
Intern speichern Attribute ihre entsprechenden Get- und Set-Funktionen. Der Attribut-Datentyp
lautet wie folgt:
data Attr w a = Attr (w -> IO a) (w -> a -> IO ())
mit Konstruktoren
newAttr :: String -> (w -> IO a) -> (w -> a -> IO ()) -> Attr w a
20
reflectiveAttr :: Typeable a => String -> (w -> IO a) -> (w -> a -> IO ())
-> Attr w a
Der Unterschied zwischen den Konstruktoren besteht darin, dass der Wert eines Attributs, das mit
reflectiveAttr erzeugt wurde, "wahrscheinlich" auch mit der Funktion getPropValue
erhalten werden kann. Da dies zu schlechtem Code führen kann, wird aber von der Benutzung
seitens der Entwickler abgeraten (laut Dokumentation). Die Verwendung von newAttr ist also
vorzuziehen, in der Instanz Textual ist dies aber beispielsweise nicht der Fall:
instance Textual (Window a) where
text = reflectiveAttr "text" getter setter
<Definition von getter und setter>
getter und setter werden hier im wesentlichen durch die Funktionen windowGetLabel
und windowSetLabel definiert (TextCtrl und ComboBox werden gesondert behandelt).
Die Funktion get hat damit eine sehr einfache Definition:
get :: w -> Attr w a -> IO a
get w (Attr getter setter) = getter w
Die folgende Definition des Datentyps der Properties erlaubt es, durch Quantifizierung[8] und
damit Verbergen der Typen der Werte, verschiedenartige Properties in eine homogene Liste zu
verpacken:
data Prop
= forall a . (:=) (Attr w a) a
| forall a . (:~) (Attr w a) (a -> a)
Hierbei wird deutlich, dass die Zuweisungsoperatoren tatsächlich Konstruktoren sind. Diese
Quantifizierung stellt allerdings eine zusätzliche Erweiterung des Haskell-98 Standards dar.
Die Funktion set ist schließlich mit Hilfe von Pattern-Matching definiert. Im Falle des (:=)Operators wird setter aufgerufen oder im Falle des (:~)-Operators, wie oben bemerkt, zunächst
getter und anschließend setter:
set :: w -> [Prop w] -> IO ()
set w props = mapM_ setone props
where
setone (Attr getter setter := x) = setter w x
setone (Attr getter setter :~ f) = do
x <- getter w
setter w (f x)
3.4 Lösung des Font-Problems
Für das in Abschnitt 2.4.6 beschriebene Font-Problem kann nun eine Übergangslösung angegeben
werden (hier: eingeschränkt auf die Font-Größe). Es wird ein Modul mit einer Instanz analog zu
Abschnitt 3.3.2 definiert:
21
{-# OPTIONS -XTypeSynonymInstances #-}
module FontSizeFix where
import Graphics.UI.WX
import Graphics.UI.WXCore
class FSize w where
fSize :: Attr w Int
instance FSize (TextCtrl a) where
fSize = newAttr "fSize" textCtrlGetFontSize textCtrlSetFontSize
In der ersten Zeile wird die unter 3.4 genannte Option des GHC angegeben, die die Restriktion einer
flachen Typdefinition aufhebt. Durch Auslagerung des Codes in ein Modul, muss dieses lediglich in
andere Module importiert werden, wenn die Lösung verwendet werden soll. Die Typklasse wird für
beliebige Widgets definiert, die Instanz konkret für ein Objekt wenigstens vom Typ TextCtrl.
Um das Attribut zu erzeugen, wird der Konstruktor newAttr mit dem Namen des Attributs und
seinen Get- und Set-Funktionen aufgerufen.
Die Get- und Set-Funktionen sind die gleichen, wie im Abschnitt 2.4.6, deshalb seien hier nur ihre
Typen angegeben:
textCtrlGetFontSize :: TextCtrl a -> IO Int
textCtrlSetFontSize :: TextCtrl a -> Int -> IO ()
textCtrlGetFont :: TextCtrl a -> IO (Font ())
textCtrlSetFont :: TextCtrl a -> Font b -> IO ()
Durch Import dieses Moduls im Modul Main vereinfacht sich der Event-Handler zur Änderung der
Font-Größe zu
textCtrlChangeFontSize :: TextCtrl a -> SpinCtrl a -> (Int -> Int -> Int)
-> IO ()
textCtrlChangeFontSize currText fontSizeSpinner fun =
do
sizeStepping <- get fontSizeSpinner selection
set currText [fSize :~ fun sizeStepping]
Durch Verwendung des (:~)-Operators wird der Code nochmals kompakter.
22
3.5 Kommunikation mit C++
Die wxWidgets-Bibliothek wurde in C++ geschrieben. Um auf dessen Schnittstelle von wxHaskell
aus zugreifen zu können, müssen also C++-Funktionen aufgerufen werden. Es gibt jedoch derzeit
keinen Haskell-Compiler, der C++-Konventionen beim Aufruf von Funktionen unterstützt. Die Idee
ist nun, C++-Funktionen wie C-Funktionen zu behandeln. Auf C-Funktionen kann mit Hilfe des
Haskell 98 Foreign Function Interface 1.0[9] zugegriffen werden.
Als Beispiel sei hier eine C-Wrapper-Funktion für die Funktion SetLabel der (C++-) Klasse
Window aufgeführt:
extern "C"
void wxWindowSetLabel(wxWindow* self, const char* text) {
self->SetLabel(text);
}
Dazu gehört eine (C-) Header-Datei:
extern void wxWindowSetLabel(wxWindow*, const char*);
Um Haskell-Typen anstelle von C-Typen nutzen zu können (Marshalling) gibt es die WrapperFunktion
windowSetLabel :: Window a -> String -> IO ()
windowSetLabel self text =
whenValid self (withCString text (wxWindowSetLabel self))
außerdem den "Fremdimport" des C-Wrappers
foreign import ccall "wxWindowSetLabel"
:: Ptr a -> Ptr CChar -> IO ()
Da wxWidgets aus mehr als 500 Klassen und 2800 Methoden besteht, wurden die Definitionen in
obiger Art und Weise nicht alle von Hand erstellt. Stattdessen wurden C-Wrapper und ihre Header
aus der wxEiffel-Bibliothek[10] benutzt. Haskell-Wrapper und "Fremdimporte" wurden mit einem
eigenen Werkzeug namens wxDirect (unter Verwendung von Parsec[11]) generiert.
Das Haskell-Programm ist nun an die C-Laufzeitbibliothek gebunden, während wxWidgets an die
C++-Laufzeitbibliothek gebunden ist. Dies kann zu Bindungsfehlern führen. Um dies zu
verhindern, wird der C++-Code in eine dynamic link library3 (DLL) kompiliert. Dadurch ist
allerdings die gesamte wxWidgets-Bibliothek in dieser DLL enthalten.
4 Bewertung
Abschließend sei eine kurze Bewertung der wxHaskell-Bibliothek in Form von Vor- und Nachteilen
angegeben. Diese beziehen sich ausschließlich auf die aktuelle Version 0.11. Es ist mit Änderungen/
Erweiterungen diesbezüglich zu rechnen.
-
imperativer Programmierstil → Seiteneffekte
-
Dokumentation stark lückenhaft
-
Typsystem scheint nicht immer zu greifen (vgl. 2.4.6)
-
teilweise Verwendung des Pakets WXCore erforderlich → niedriges Abstraktionsniveau
3 In einer dynamic link library (dynamische Bibliothek) werden Programme referenziert, das heißt dynamisch
gebunden (und nicht kopiert, vgl. statisches Binden). Bei Bedarf werden diese dann zur Laufzeit vom LaufzeitBinder automatisch geladen.
23
+ Typsicherheit und Abbildung der Vererbungshierarchie → aussagekräftige Fehlermeldungen
+ hohes Abstraktionsniveau (Paket WX) → flexibel, leicht erlernbar
+ funktionale Berechnungen möglich
+ auf ausgereifter Bibliothek (wxWidgets) aufbauend → stabil, natives Look & Feel
Literaturverzeichnis
[1] Daan Leijen, wxHaskell: A Portable and Concise GUI Library for Haskell, 2004,
http://research.microsoft.com/en-us/um/people/daan/download/papers/wxhaskell.pdf
[2] J. Smart, R. Roebling, V. Zeitlin, R. Dunn, et. al., The wxWidgets library, ...,
http://www.wxwidgets.org
[3] D. Leijen, The wxHaskell library, ..., http://wxhaskell.sourceforge.net
[4] A. Barendsen, S. Smetsers, Uniqueness Type Inference in "7th International Symposium on
Programming Language Implementation and Logic Programming (PLILP’95)", Springer-Verlag,
1995
[5] P. Wadler, Linear types can change the world! in "IFIP TC 2 Working Conference on
Programming Concepts and Methods", North Holland, 1990
[6] D. Leijen, The lambda Abroad – A Functional Approach to Software Components in "PhD
thesis", Universiteit Utrecht, 2003
[7] D. Leijen, E. Meijer, Domain specific embedded compilers in "Second USENIX Conference on
Domain Specific Languages(DSL’99)", USENIX Association, 1999
[8] K. Läufer, Type classes with existential types in "Journal of Functional Programming", Mai
1996
[9] M. Chakravarty, S. Finne, F. Henderson, M. Kowalczyk, D. Leijen, S. Marlow, E. Meijer, S.
Panne, S. Peyton-Jones, A. Reid, M. Wallace, M. Weber, The Haskell 98 foreign function interface
1.0: an addendum to the Haskell 98 report, 2003, http://www.cse.unsw.edu.au/˜chak/haskell/ffi
[10] U. Sander et. al., The wxEiffel library, , http://wxeiffel.sourceforge.net
[11] D. Leijen, E. Meijer, Parsec: Direct style monadic parser combinators for the real world in
"Technical Report UUCS-2001-27", Universiteit Utrecht, 2001
24
Herunterladen