Praktikum BKSPP Aufgabenblatt Nr. 4 - Goethe

Werbung
PD Dr. David Sabel
Institut für Informatik
Fachbereich Informatik und Mathematik
Johann Wolfgang Goethe-Universität Frankfurt am Main
Praktikum BKSPP
Wintersemester 2014/15
Aufgabenblatt Nr. 4
Abgabe: Dienstag, 10. Februar 2014
Für dieses Aufgabenblatt verwenden wir das Webframework Yesod
http://www.yesodweb.com
welches es ermöglicht dynamische Webseiten in Haskell zu programmieren.
Neben zahlreichen Internetblogs gibt es als wesentliche Referenzen zu Yesod:
• Das Buch „Haskell and Yesod“, das auch online unter
http://www.yesodweb.com/book
verfügbar ist.
• Zum schnellen Einstieg gibt es unter
http://yannesposito.com/Scratch/en/blog/Yesod-tutorial-for-newbies
ein Tutorial.
Das Buch und das Tutorial unterscheiden sich u.a. darin, wie das Anlegen des Quellcodes
geschieht: Während das Buch den Quellcode meistens „per Hand“ editiert, verwendet das
Tutorial das yesod-Tool, um sogenannte Handler automatisiert hinzuzufügen. Während
die erste Methode zu mehr Verständnis führt, bietet die zweite Methode eine einheitliche
Vorgehensweise.
Yesod verwendet an vielen Stellen sogenanntes Template Haskell, dessen Quelltext noch
kein gültiger Haskell-Code ist, sondern aus dem während des Kompilierens erst gültiger Code erzeugt wird. Yesod benutzt viele Erweiterungen des Typsystems von Haskell
(wie sie im GHC verwendet werden). Daher ist es oft erforderlich, die Typen von Funktionen explizit anzugeben, da die entsprechenden Erweiterungen keine Typinferenz (also
automatisches Ausrechnen der Typen) zulassen.
Das Aufgabenblatt unterscheidet sich auch vom Aufbau von den vorherigen Blättern: Zunächst wird die Verwendung von Yesod an Beispielen erläutert und einige bekannte Fallen und Probleme diskutiert. Erst im Anschluss daran befindet sich die eigentliche Aufgabenstellung. Zur Bearbeitung ist empfohlen, zunächst die Beispiele selbst auszuprobieren
und nachzuvollziehen und danach die Arbeiten in der Gruppe aufzuteilen.
1
Einführung zu Yesod
1
Installation
Zunächst muss Yesod installiert werden, es liegt nicht der Haskell-Platform bei. Die Installation erfolgt über das Paketverwaltungstool Cabal, welches mit der Haskell-Platform
installiert ist.
Zunächst sollte man prüfen, dass das benutzereigene cabal-Verzeichnis vom System gefunden wird: Unter Linux-Systemen ist das üblicherweise das Verzeichnis
$HOME/.cabal/bin und unter Windows das Verzeichnis %appdata%\cabal\bin. Diese Verzeichnisse sollten in der Umgebungsvariablen PATH eingetragen werden, damit die Programme dort vom System gefunden werden.
Zum Installieren von Yesod sind die folgenden drei Befehle in einer Konsole einzugeben,
wobei eine Internetverbindung benötigt wird, damit die Pakete und die Paketdatenbank
heruntergeladen werden kann:
cabal update
cabal install cabal-install
cabal install yesod-bin
Die erste Zeile sorgt dafür, dass die Paketverwaltung cabal die aktuellsten Pakete kennt.
Die zweite Zeile aktualisiert das cabal-Werkzeug auf den neuesten Stand. Die dritte Zeile
installiert dann schließlich Yesod. Die beiden Installationen können durchaus einige Zeit
in Anspruch nehmen.
Hinweis. Falls es zu Problemen kommt, sollte man prüfen, ob im jeweiligen System die Umgebungsvariablen so gesetzt sind, dass ausführbare Dateien im eigenen cabal-Verzeichnis gefunden
werden und vor anderen (System-)Dateien gefunden werden, insbesondere bevor die Systemverzeichnisse der Haskell-Platform-Installation geprüft werden!
Hat die Installation funktioniert, so sollte das System den Befehl yesod kennen, was man
in einer Konsole überprüfen kann, indem man yesod eintippt:
> yesod
Usage: yesod [-d|--dev] [-v|--verbose] COMMAND
2
Anlegen eines Projekts
Im aktuellen Pfad (dort wo das Projekt gespeichert werden soll) gibt man
yesod init
2
ein. Anschließend wird ein Projektname und die Datenbankanbindung abgefragt. Hier
sollte im Vorausblick auf die Aufgabenstellung als Projektname Quiz und für die Datenbankanbindung ’s’ für sqllite gewählt werden:
> yesod init
Welcome to the Yesod scaffolder.
I’m going to be creating a skeleton Yesod project for you.
What do you want to call your project? We’ll use this for the cabal name.
Project name: Quiz
Yesod uses Persistent for its (you guessed it) persistence layer.
This tool will build in either SQLite or PostgreSQL or MongoDB support for you.
We recommend starting with SQLite: it has no dependencies.
s
p
pf
mongo
mysql
simple
url
=
=
=
=
=
=
=
sqlite
postgresql
postgresql + Fay (experimental)
mongodb
MySQL
no database, no auth
Let me specify URL containing a site (advanced)
So, what’ll it be? s
Yesod legt das Verzeichnis Quiz an und gibt einige Meldungen aus, insbesondere die
Kommandos, die zum Erzeugen des Webframeworks notwendig sind. Diese sind:
cd Quiz
zum Wechseln in das neue Verzeichnis.
cabal sandbox init
Hierdurch wird eine sogenannte „Cabal-Sandbox“ angelegt. Diese führt dazu, dass Cabal
in diesem Verzeichnis sämtliche benötigten Pakete lokal ablegt, ohne die globale Konfiguration anzufassen.
cabal install --enable-tests . yesod-platform yesod-bin --max-backjumps=-1 --reorder-goals
Hierdurch werden die Pakete yesod-bin und yesod-platform lokal in der Sandbox installiert. Das Installieren dauert erneut einige Zeit.
Verläuft dies erfolgreich, kann das Webframework (im Entwicklungsmodus) mit
yesod devel
gestartet werden. Öffnen Sie im Browser die URL http://localhost:3000. Sie erhalten
dann entweder eine Meldung, dass das Webframework gerade kompiliert wird oder Sie
erhalten eine Startseite. Üblicherweise kompiliert Yesod die Webseiten im Hintergrund
und es dauert eine Weile bis eine Seite mit der Überschrift "Hello" erscheint. Insbesondere läuft daher ein lokaler Webserver auf Port 3000, etwaige Meldungen der Firewall
sollten so beantwortet werden, dass sie die Kommunikation mit dem Webserver ermöglichen.
Ist dieser Punkt erreicht, so funktioniert Yesod und die Programmierung kann beginnen.
3
3
Erste Schritte
Zunächst sei erläutert, was Yesod im Modus yesod devel macht: Der Yesod-Webdienst
wird bei jedem Speichern einer der Dateien im Verzeichnis Quiz automatisch neu kompiliert. Insbesondere können im Konsolen-Fenster Fehlermeldungen und Warnungen gelesen werden, ohne dass der Dienst jedesmal neu gestartet werden muss. Zum Kompilieren
verwendet Yesod den Paket-Manager cabal, dessen Hauptkonfiguration für das YesodProjekt in der Datei Quiz.cabal zu finden ist. In einigen wenigen Fällen, muss man diese
Datei auch per Hand anpassen (z.B. wenn neue Quellcode-Dateien dem Projekt hinzugefügt werden oder Abhängigkeiten fehlen). In diesen Fällen sollte Yesod neu gestartet werden (erst Eingabetaste zum Beenden betätigen, dann erneut yesod devel eingeben), da
die Änderungen nicht immer von Yesod erkannt werden. Da Yesod im Entwicklungsmodus ständig neu kompiliert, empfiehlt es sich eine zweite Konsole im gleichen Verzeichnis
zu starten, um etwaige Kommandos eingeben zu können.
3.1 Text statt String
Yesod und auch wir verwenden an vielen Stellen den Typ Text aus dem Modul Data.Text
anstelle von Strings, denn Data.Text ist teilweise effizienter und viele Yesod-Funktionen
sind darauf abgestimmt. Daher importieren wir das Modul zunächst in Foundation.hs,
d.h. wir fügen dort die folgende Zeile hinzu:
import Data.Text
In anderen Modulen ist es oft nicht sinnvoll Data.Text unqualifiziert zu importieren, da
die Namen der Bibliotheksfunktionen mit Funktionen aus der Prelude und aus Data.List
überlappen. Daher sollte qualifiziert importiert werden, z.B. mit
import qualified Data.Text as T
Z.B. kann mit T.head das erste Zeichen eines Texts berechnet werden. Zwei weitere hilfreiche Funktionen sind pack :: String -> Text zum Konvertieren eines Strings in einen
Text und unpack :: Text -> String zum Konvertieren eines Texts in einen String. Mit
qualifiziertem Import würden die Funktionen durch T.pack bzw. T.unpack aufgerufen.
3.2
Wesentliche Dateien im Projekt
Neben der bereits erwähnten cabal-Konfigurationsdatei Quiz.cabal und dem Modul
Foundation gibt es im Verzeichnis Quiz nun eine Menge von Verzeichnissen und Dateien.
Die wesentlichen Dateien und Verzeichnisse sind:
config/routes
Handler/
templates/
config/models
4
• In der Datei config/routes wird festgelegt, welche URLs der Webdienst behandelt,
und wie diese Behandlung durch Haskell-Funktionen durchgeführt wird. Es werden daher die Routen von URL zu Handler festgelegt.
• Im Verzeichnis Handler/ sind jene Dateien, die den Code der Handler enthalten.
• Im Verzeichnis templates/ sind HTML, JavaScript und CSS Vorlagen (templates)
im Hamlet-, Lucius-, bzw. Julius-Format.
• In der Datei config/models wird die Datenbankanbindung konfiguriert, d.h.dort
werden die Datentypen angelegt, die in der Datenbank als Datenbanktabellen abgespeichert werden.
3.3
Eine erste Seite
In diesem einfachen Beispiel erzeugen wir eine Seite, die es ermöglichen soll mit dem Abruf der URL http://localhost:3000/echo/irgendeinText den Text "irgendeinText"
auf der Webseite wieder zu geben.
Hierfür rufen wir (im Verzeichnis Quiz)
yesod add-handler
auf. Yesod übernimmt hierbei einiges an Arbeit, was sonst per Hand programmiert werden müsste. Wir werden gefragt, wie die Route des neuen Handlers lauten soll. Der Name
ist nicht der URL-Teil "echo", sondern ein beliebiger Name, der im Programm verwendet wird. Wir verwenden "Echo". Als nächstes müssen wir ein Muster für die Route eingeben. Dies ist die gewünschte URL und das Format von "irgendeinText". Wir geben
/echo/#Text ein, da wir den Pfad "echo" verwenden möchten und beliebiger Text (vom
Typ Text aus dem Modul Data.Text) durch das Pattern #Text dargestellt wird.
Schließlich müssen wir noch die verwendeten HTTP-Methoden angeben. Hier genügt es
die GET-Methode zu verwenden.
Quiz/> yesod add-handler
Name of route (without trailing R): Echo
Enter route pattern (ex: /entry/#EntryId): /echo/#Text
Enter space-separated list of methods (ex: GET POST): GET
Hinweis. Wesentlichen Methoden des HTTP-Protokolls zum Senden von Daten des Clients an
den Server sind GET und POST. Bei der GET-Methode werden die Daten direkt mit der URL übertragen (wie im obigen Beispiel der Text irgendeinText), während bei der POST-Methode die
Daten unsichtbar (d.h. nicht als Teil der URL) übertragen werden. Die Datenmenge ist bei GET
begrenzt, bei POST unbegrenzt. Zum Übermitteln von Formulardaten oder ähnlichem sollte die
POST-Methode verwendet werden, die GET-Methode sollte bei reinem statischem Abruf von Informationen verwendet werden.
5
Durch das Anlegen des Handlers verändert Yesod die Datei config/routes, indem dort
der Eintrag
/echo/#Text EchoR GET
angehängt wird (das "R" fügt Yesod hinzu). Die Zeile enthält das URL-Muster, den
Handler-Namen und die HTTP-Methode. Außerdem legt Yesod die Haskell-Datei
Handler/Echo.hs an, importiert das entsprechende Modul (Handler.Echo) in der Hauptdatei Application.hs und verändert die Cabal-Konfiguration, so dass Handler.Echo nun
mit zum Projekt gehört und daher auch kompiliert wird.
Nach erneuter Kompilierung (dies sollte Yesod im Hintergrund automatisch tun), kann
man die Seite http://localhost:3000/echo/irgendeinText im Browser aufrufen. Man
erhält dann die Meldung, dass die Funktion getEchoR noch nicht implementiert ist.
Schaut man in das Modul Hander.Echo, so sieht man dies auch:
module Handler.Echo where
import Import
getEchoR :: Text -> Handler Html
getEchoR = error "Not yet implemented: getEchoR"
D.h. wir müssen getEchoR implementieren, um die gewünschte Funktionalität zu erhalten. Der Typ von getEchoR verrät, dass wir als Eingabe einen Text erhalten, dies ist gerade der an der URL angehängt Text (z.B. irgendeinText). Die getEchoR-Funktion ist
eine monadische Funktion, die jedoch nicht die bereits bekannte IO-Monade, sondern die
Handler-Monade verwendet. Die Rückgabe der Funktion ist eine HTML-Seite (vom Typ
HTML) in der Handler-Monade.
Hinweis. In der Handler-Monade kann man IO-Aktionen der IO-Monade auch ausführen (da
Handler ein sogenannter MonadTransformer ist (der mehrere Monaden vereint und daher auch
die IO-Monade beinhaltet). Zum Ausführen von IO-Aktionen in der Handler-Monade muss man
die IO-Aktion allerdings in die Monade „liften“. Dies geschieht, indem man die Funktion liftIO
verwendet, z.B. schreibt man liftIO (putStrLn "Eine Ausgabe"), um die monadische IOAktion putStrLn "Eine Ausgabe" in die Handler-Monade zu liften.
Eine mögliche Implementierung von getEchoR ist:
getEchoR txt = defaultLayout [whamlet|<h1>#{txt}|]
Was passiert hier? Die Funktion defaultLayout erzeugt eine HTML-Seite im StandardLayout der Webseite aus einem Template. Das Template hier ist [whamlet|<h1>#{txt}|],
wobei whamlet festlegt, dass es sich um ein Hamlet-Widget handelt. Hamlet-Templates
dienen dazu HTML-Templates zu beschreiben.
Im Grunde wird durch <h1>#{txt} die HTML-Zeile
6
<h1>DerTextAusDerEingabe</h1>
erzeugt, wenn "DerTextAusDerEingabe" an die getEchoR-Funktion übergeben wird, d.h.
mit #{txt} wird auf den Wert des Haskell-Ausdrucks txt zugegriffen.
Nach Ändern und Speichern der Datei Handler/Echo.hs kompiliert Yesod die Webseite
neu und der Aufruf von http://localhost:3000/echo/irgendeinText im Browser erzielt den gewünschten Effekt.
4
Templates
In diesem Abschnitt beschreiben wir im Wesentlichen Hamlet-Templates zum Erzeugen
von HTML-Seiten, Yesod stellt auch Lucius-Templates zum Erzeugen von Cascading Style
Sheets (CSS) und Julius-Templates zum Erzeugen von JavaScript-Code zur Verfügung.
Genauere Details sind z.B. unter der folgenden URL zu finden:
http://www.yesodweb.com/book/shakespearean-templates
Das Einfügen der Hamlet-Templates (und auch anderer Templates) im Code selbst ist eher
als unsaubere Methode zu sehen, da die Trennung zwischen Code und Template verwischt. Daher ist es besser, das Template auszulagern.
Daher legen wir im Verzeichnis template/ eine Datei echo.hamlet mit dem Inhalt
<h1> #{txt}
an und ersetzen getEchoR in Handler/Echo.hs durch den Code
getEchoR txt = defaultLayout $(widgetFile "echo")
Hinweis. $( ... ) ist hierbei Syntax von Template Haskell und nicht zu verwechseln mit
$ ( ... ), dem Anwendungsoperator $ und „normalen Klammern“.
Jetzt können wir das Template echo.hamlet weiter bearbeiten z.B. durch
<h1> Der eingegebene Text ist #{txt}
<ul>
<li> Der Text hat #{T.length txt} Zeichen
<li> und lautet in Großbuchstaben: #{T.toUpper txt}
<p> Ein neuer Absatz.
Dieser Text ist im gleichen Absatz
Dieser Text nicht mehr, da Hamlet Einrückungen ernst nimmt.
<p>
<a href=@{EchoR "EinAndererText"}> Ein Link mit anderem Text
wobei wir im Modul Handler.Echo das Modul Data.Text importieren durch
7
import qualified Data.Text as T
Das Hamlet-Template verwendet nur öffnende HTML-Tags, die schließenden werden automatisch hinzugefügt. Hierbei zählt die Einrückung, z.B. führt
<ul>
<li> 1.Eintrag
<li> 2.Eintrag
<li> 3.Eintrag
zum ungültigen HTML-Code (Listenelemente <li>...</li> außerhalb von <ul>)
<ul>
<li> 1.Eintrag</li>
<li> 2.Eintrag</li>
</ul>
<li> 3.Eintrag</li>
während
<ul>
<li> 1.Eintrag
<li> 2.Eintrag
<li> 3.Eintrag
den gültigen (und vermutlich gemeinten) Code
<ul>
<li> 1.Eintrag</li>
<li> 2.Eintrag</li>
<li> 3.Eintrag</li>
</ul>
erzeugt.
Mit #{ code } wird der Haskell-Code code in das Template eingebunden und sein Wert
angezeigt und eingefügt. Dabei dürfen alle Funktionen verwendet werden, die im entsprechenden Modul (also Handler.Echo) bekannt sind, sowie lokale Namen, die im Gültigkeitsbereich von $(widgetFile "echo") stehen (wie die Variable txt im Beispiel).
Ein anderes Feature zeigt die Zeile
<a href=@{EchoR "EinAndererText"}> Ein Link mit anderem Text
Mit @{ Route } kann die Route innerhalb des Webservers angegeben werden, Yesod setzt
diese automatisch in die URL /echo/EinAndererText um.
Ein anderes Feature innerhalb von Hamlet-Templates sind einfache if-then-else Blöcke:
$if code
HTMLcode1
$else
HTMLcode2
8
Wenn code zu True auswertet, wird der HTMLcode1 angezeigt, anderenfalls HTMLcode2.
Wir können echo.hamlet z.B. ab ändern in
<h1> Der eingegebene Text ist #{txt}
<p>
$if T.length txt > 20
der eingegebe Text ist sehr lang
$else
der eingebene Text ist eher kurz
Je nach Länge des Texts wird die eine oder die andere Ausgabe erzeugt.
Mithilfe der Syntax $forall x <- xs kann über Listen iteriert werden: Z.B. können wir
echo.hamlet abändern in
<h1> Der eingegebene Text ist #{txt}
<ul>
$forall x <- T.unpack txt
<li> #{x}
Damit werden alle Buchstaben des übergebenen Texts einzeln als Liste angezeigt.
Schließlich können Hamlet-Templates mithilfe von CSS gestylt werden.
Hierfür gibt es die Spezialsyntax für Attribute .name und #name, die das HTML-Attribut
class="name" bzw. id="name" erzeugen.
CSS-Templates können durch Lucius-Templates erstellt werden. Ein Hamlet-Template
name.hamlet wird automatisch mit dem lucius-Template name.lucius verknüpft. Z.B.
können wir in templates/ eine Datei echo.lucius mit dem Inhalt
.red {
color:red;
background-color:black;
}
anlegen. Ändern wir echo.hamlet ab in
<h1> Der eingebene Text ist
<span .red> #{txt}
so erscheint der eingegebene Text in roter Schrift auf schwarzem Hintergrund.
Schließlich gibt es noch Julius-Templates für JavaScript-Dateien, auf die wir
hier jedoch nicht weiter eingehen (siehe z.B. http://www.yesodweb.com/book/
shakespearean-templates).
9
5
Ein Beispiel mit Formulardaten und der POST-Methode
Betrachten wir als weiteres Beispiel eine kleine Anwendung, die zunächst in einem Formular einen Namen abfragt und anschließend eine Begrüßung ausgibt. Die Route hierzu
sei /hallo und der Handler HalloR, und es werden sowohl die GET als auch die POSTMethode verwendet.
yesod add-handler
Name of route (without trailing R): Hallo
Enter route pattern (ex: /entry/#EntryId): /hallo
Enter space-separated list of methods (ex: GET POST): GET POST
Im durch Yesod erzeugten Modul Handler.Hallo müssen nun die Funktionen getHalloR
für die Abarbeitung einer GET-Anfrage und postHalloR für die Abarbeitung einer POSTAnfrage implementiert werden.
Die GET-Anfrage stellt das Formular zur Verfügung, die POST-Anfrage wird beim Absenden des Formulars verwendet und muss die Formular-Daten lesen und den Begrüßungstext erzeugen. Eine Implementierung der getHalloR-Funktion (ohne ausgelagerte
Hamlet-Datei) ist:
getHalloR = do
defaultLayout [whamlet|<form method=post action=@{HalloR}>
<h1>Formular
<p>
Dein Vorname:
<input type=text name=Vorname>
<p>
Dein Nachname:
<input type=text name=Nachname>
<p>
<input type=submit>
|]
Die Zeile <form method=post action=@{HalloR}> besagt, dass es sich um ein HTMLFormular handelt, welches die POST-Methode verwendet und als Aktion den Handler
HalloR also die URL /hallo aufruft. Anschließend werden je ein Eingabefeld für Text mit
Namen Vorname und ein weiteres mit Namen Nachnamen und schließlich noch ein Button
zum Absenden des Formulars definiert.
Die postHalloR-Funktion muss die über die POST-Methode erhaltenen Daten einlesen
und verarbeiten. Hierfür stellt Yesod im wesentlichen die Funktionen ireq und iopt zur
Verfügung.
• ireq zeigt an, dass die entsprechende Eingabe benötigt wird.
• iopt zeigt an, dass die entsprechende Eingabe optional ist (die Rückgabe ist daher
mit einem Maybe-Typen verpackt).
10
Sowohl ireq als auch iopt erwarten zwei Argumente: Das erste Argumente ist der Feldtyp und das zweite Argument ist der Name des Felds (wie er im Attribut angegeben ist).
Der Feldtyp kann z.B. textField sein, so dass die Rückgabe ein Text ist, vordefiniert
ist z.B. auch intField, der ein Int zurück liefert. Eine entsprechende Anfrage wird mit
runInputPost abgearbeitet. Z.B.
postHalloR = do
v <- runInputPost (ireq textField "Vorname")
defaultLayout [whamlet|<p> Hallo #{v}
|]
In diesem Fall wird nur der Vorname abgefragt und ausgegeben. Zur Verknüpfung mehrerer Abfragen, muss dass sogenannte „Applicative-Interface“1 verwendet werden. Dieses stellt die beiden Kombinatoren pure und <*> bereit, die etwas gewöhnungsbedürftig
sind: Für eine Struktur (einen Typkonstruktor) m sind die Typen der beiden Operatoren:
pure :: a -> m a
<*> :: m (a -> b) -> m a -> m b
Der Operator pure verpackt einen beliebigen Typ in den gelifteten Typ. Der Operator <*> ist die sequentielle Anwendung, insbesondere sind hier Ausdrücke der Form
(pure f) <*> a1 <*> ... <*> an zu betrachten, wobei f eine n-stellige (pure) Funktion ist, und a1,. . . ,an sind „Aktionen“. Die Auswertung des Ausdrucks führt die Aktionen
a1,. . . ,an sequentiell nacheinander aus und wendet anschließend die Ergebnisse auf f an.
Das Auslesen der beiden Feldwerte für den Vor- und Nachnamen kann daher durch
(pure pair) <*> (ireq textField "Vorname") <*> (ireq textField "Nachname")
durchgeführt werden, wobei pair die Funktion
pair a b = (a,b)
ist. Als Abkürzung für (pure f) <*> a1 <*> ... <*> an kann man
f <$> a1 <*> ... <*> an schreiben, da der Operator <$> definiert ist als:
auch
<$> :: (a -> b) -> m a -> m b
f <$> a = (pure f) <*> a
Insgesamt ergibt dies als Implementierung von postHalloR:
1
siehe z.B. http://staff.city.ac.uk/~ross/papers/Applicative.pdf und https://www.haskell.
org/haskellwiki/Applicative_functor
11
postHalloR :: Handler Html
postHalloR = do
(v,n) <- runInputPost (pair <$> (ireq textField "Vorname")
<*> (ireq textField "Nachname"))
defaultLayout [whamlet|<p> Hallo #{v} #{n}
|]
where pair a b = (a,b)
6
Datenbank-Anbindung
Zum Speichern von Daten stellt Yesod eine Datenbankanbindung zur Verfügung. Yesod
verwendet dabei das Persistent-Interface (siehe z.B. http://www.yesodweb.com/book/
persistent) Beim Erstellen des Projekts mussten wir angeben, welche Datenbankanbindung wir verwenden möchten. Dort haben wir sqllite ausgewählt. Dabei werden die
Daten direkt lokal in einer Datei (in unserem Fall Quiz.sqlite3) abgelegt.
Die Verknüpfung von Datenbanktabellen und Haskell-Datentypen wird in der Konfigurationsdatei config/models bewerkstelligt. Möchten wir eine einfache Tabelle speichern,
die Vor- und Nachnamen speichert, so können wir dies durch die folgenden Zeilen in der
Datei config/models bewerkstelligen:
Person
vorname Text
nachname Text
Dies legt eine Tabelle Person an, die zwei Attribute für den Vor- und Nachnamen jeweils
als Text besitzt. Außerdem wird automatisch ein Attribut für einen Identifier (Schlüssel)
(namens PersonId) angelegt.
Auf der Haskell-Ebene existiert hierdurch ein Datentyp der Form
data Person = Person {personVorname :: Text, personNachname :: Text}
Man beachte, wie sich die Attributsnamen zusammensetzen: Der Name des Datentyps
(Person) in Kleinschreibung und die Tabellenattribute mit beginnendem Großbuchstaben.
Desweiteren wird ein Typ PersonId automatisch erzeugt
Einige wesentliche Methoden zum Datenbankzugriff sind:
• Innerhalb der Handler-Monade werden die Zugriffe als Argument der Funktion
runDB durchgeführt.
• Mit get und einer Id als Argument wird nach dem Eintrag mit der entsprechenden
Id in der Datenbank gesucht. Das Ergebnis ist dann schon vom richtigen Typ (verpackt durch den Maybe-Typen). Z.B. können wir eine Funktion definieren, die eine
Person aus der Datenbank abfragt:
12
getPerson :: PersonId -> Handler (Maybe Person)
getPerson personid = runDB (get personid)
• Mit insert und einer Person als Argument kann ein neuer Eintrag in die Datenbank
geschrieben werden. Die Rückgabe ist dabei die neu erzeugte Id. Z.B. können wir
eine Funktion definieren, die das für Personen bewerkstelligt:
insertPerson :: Person -> Handler (PersonId)
insertPerson p = runDB (insert p)
• Mit delete und einer PersonId als Argument wird der entsprechende Eintrag in
der Datenbank gelöscht, z.B. können wir definieren:
deletePersonById :: PersonId -> Handler ()
deletePersonById pid = runDB (delete pid)
• Mit update können die einzelnen Attribute eines Eintrags aktualisiert werden. Dabei wird als erstes Argument die Id des Datensatzes übergeben und als zweites
Argument eine Liste der neuen Attributwerte. Dabei wird eine Aktualisierung als
TypnameAttribut =. neuerWert geschrieben, z.B. können wir eine Funktion zum
Aktualisieren des Vornames schreiben als:
updateVorname :: PersonId -> Text -> Handler ()
updateVorname pid neuerName =
runDB (update pid [PersonVorname =. neuerName])
Hierbei ist zu beachten, dass in diesem Fall PersonVoname in Großschreibung erfolgt (also verschieden von der Schreibweise der Zugriffsfunktion personVorname!).
Außerdem wird der in Yesod vordefinierte Operator =. im Sinne einer Zuweisung
verwendet.
• Mit selectList filter selectopts können alle Einträge der Datenbank als Liste abgefragt werden. Durch die Argumente filter und selectopts können einerseits nur bestimmte Einträge gefiltert und andererseits die Ausgabe sortiert oder auf
eine bestimmte Anzahl begrenzt werden. (siehe http://www.yesodweb.com/book/
persistent für genauere Details). Der Aufruf selectList [] [] führt dazu, dass
sämtliche Datenbankeinträge geliefert werden. Z.B. können wir dies für Personen
implementieren:
getAllPersonsDB :: Handler ([Entity Person])
getAllPersonsDB = runDB (selectList [] [])
Der Typ dieser Funktion ist nicht wie erwartet Handler [Person], sondern
Handler ([Entity Person]). Hierbei verpackt der Entity-Typ die Person zusammen mit ihrem Schlüssel, und man kann ihn sich für das Beispiel vorstellen (in Wirklichkeit ist Entity ein Phantomtyp) als:
13
data Entity Person = Entity PersonId Person
z.B. können wir Funktionen definieren, die nur die Ids oder nur die Personen (ohne
Ids) zurück liefern:
getAllPersonIds :: Handler ([PersonId])
getAllPersonIds = do
list <- getAllPersonsDB
return (map (\(Entity pid person) -> pid) list)
getAllPersons :: Handler ([Person])
getAllPersons = do
list <- getAllPersonsDB
return (map (\(Entity pid person) -> person) list)
Z.B. können wir die eingegebenen Daten des Formulars in der Datenbank speichern:
postHalloR :: Handler Html
postHalloR =
do (v,n) <- runInputPost (pair <$> (ireq textField "Vorname")
<*> (ireq textField "Nachname"))
insertPerson (Person {personVorname = v, personNachname = n})
defaultLayout [whamlet|<p> Hallo #{v} #{n}
|]
where pair a b = (a,b)
Fügen wir die Route PersonsR mit der Url /persons und der GET-Methode unserem Framework hinzu, so können wir mit der folgenden Implementierung von getPersonsR, alle
in der Datenbank vorkommenden Personen in einer Tabelle anzeigen:
getPersonsR :: Handler Html
getPersonsR = do
personen <- getAllPersons
defaultLayout [whamlet|<table>
$forall x <- personen
<tr>
<td>#{personVorname x}
<td>#{personNachname x}
|]
Manchmal muss man mit den Schlüsseln selbst arbeiten, daher erläutern wir noch kurz
den PersonId-Typ. Er ist definiert als Typsynonym:
type PersonId = Key Person
14
Key ist ein vordefinierter Phantom-Typ, das Argument Person wird dabei nicht verwendet, aber es zeigt an, dass es sich um einen Schlüssel für Personen handelt. Man kann sich
Key vorstellen als
data Key a = Key {unKey = PersistValue}
PersistValue ist ein Datentyp für persistente Werte. In unserem Beispiel wird automatisch ein verpackter Int64-Wert erzeugt, der als PersistInt64 i dargestellt wird, wobei
i ein Integerwert ist. Z.B. können wir eine Funktion implementieren, die Integer-Werte
in PersonId und umgekehrt konvertieren:
intToPersonId :: Integer -> PersonId
intToPersonId i = Key (PersistInt64 (fromIntegral i))
personIdToInt :: PersonId -> Integer
personIdToInt pid = case (unKey pid) of
PersistInt64 i -> fromIntegral i
_
-> error "kein PersistInt64-Wert"
Update: In der neuesten Yesod-Version git es den Konstruktor Key und die Zugriffsfunktion unKey nicht mehr in dieser Form.
In der neuesten Version funktioniert hingegen:
import Database.Persist.Class
import Database.Persist.Sql
import Data.Int
intToPersonId :: Integer -> PersonId
intToPersonId i = fromBackendKey (SqlBackendKey (fromIntegral i))
personIdToInt :: PersonId -> Integer
personIdToInt pid = fromIntegral (unSqlBackendKey $ toBackendKey pid)
D.h. (bei Verwendung des sqllite-Backends) wird anstelle von Key nun SqlBackendKey
verwendet, welcher anschließend mit fromBackendKey wieder in die interne Darstellung
der Schlüssel konvertiert werden muss.
Analog wird undSqlBackendKey statt unKey verwendet, wobei vorher die interne Darstellung der Schlüssel mit toBackendKey in einen BackendKey konvertiert wird.
7
Sessions und Cookies
Manchmal möchte man Daten einer Sitzung mit dem Webframework zwischenspeichern,
sodass die Daten während der Sitzung verfügbar sind, aber nicht permanent gespeichert
werden (sonst könnten wir die Datenbankanbindung verwenden).
15
Hierfür verwendet Yesod Cookies, die auf der Client-Seite abgelegt werden und zum Server gesendet werden. Im wesentlichen werden in den Cookies Schlüssel-Wert-Paare abgespeichert (beides als Texte) und diese können mit Yesod abgefragt werden.
Wesentliche Methoden sind (für mehr Details siehe http://www.yesodweb.com/book/
sessions):
• lookupSession erwartet einen Text (den Schlüssel) und liefert den Wert dazu, verpackt als Maybe-Typ: Wenn der Wert existiert wird Just Wert zurück geliefert, ansonsten Nothing.
• setSession erwartet zwei Texte (den Schlüssel und den Wert) und fügt diesen in
das Cookie ein. Existiert der Schlüssel schon, so überschreibt setSession den vorherigen Wert.
Als Beispiel können wir zählen, wie oft ein Benutzer die Seite mit dem Formular aufgerufen hat:
getHalloR :: Handler Html
getHalloR = do
wert <- lookupSession "anzahlAufrufe"
let anzahl = case wert of
Nothing -> 1
Just i -> 1+(read (T.unpack i))::Int
setSession "anzahlAufrufe" (T.pack (show anzahl))
defaultLayout [whamlet|<form method=post action=@{HalloR}>
<h1>Formular (Ihr #{anzahl}.Aufruf)
<p>
Dein Vorname:
<input type=text name=Vorname>
<p>
Dein Nachname:
<input type=text name=Nachname>
<p>
<input type=submit>
|]
Hinweis. Per Standardwert läuft eine Sitzung nach zwei Stunden ab. Dieser Wert kann in
Foundation.hs bei der Instanzdefinition instance Yesod App geändert werden.
8
Verwaltung der Quelldateien mit CVS
In das CVS sollten nicht alle Dateien des Projekts eingecheckt werden, insbesondere
da dies zu Problemen führt, wenn verschiedene Versionen des GHC und der HaskellPlatform verwendet werden. Die folgenden Dateien und Verzeichnisse sollten nicht eingecheckt werden:
• dist/
• static/tmp/
16
•
•
•
•
•
•
static/combined/
config/client_session_key.aes
cabal-dev/
yesod-devel/
.cabal-sandbox
cabal.sandbox.config
17
Aufgaben
Als Aufgabe soll für dieses Aufgabenblatt ein Quiz als Webanwendung mit Yesod programmiert werden. Dabei soll einerseits das Quiz gespielt werden können und der Punktestand angezeigt werden, und andererseits eine Verwaltung der Quizfragen implementiert werden.
Ein Quizspiel besteht aus 10 Fragen, die nacheinander beantwortet werden müssen. Für
jede Frage erhält der Spieler 4 mögliche Antworten unter denen er eine auswählt. Bei
richtiger Antwort erhält der Spieler für die Frage 1 Punkt. Der Endpunktestand ergibt
sich aus der Summe der Punkte aller 10 Fragen.
Aufgabe 1 (Datenbankanbindung). Fügen Sie der Datenbank den folgenden Datentyp für Fragen hinzu, indem Sie die Datei config/models um die folgenden Zeilen erweitern:
Question
question
answer1
answer2
answer3
answer4
right
deriving
Text
Text
Text
Text
Text
Int
Show
Hier stellt question den Text der Frage dar, answer1 bis answer4 repräsentieren die Texte der
vier Antwortmöglichkeiten und right ist eine Zahl (zwischen 1 und 4), die angibt, welche der vier
Antworten die richtige Antwort ist.
Beachten Sie auch, dass Yesod automatisch einen Typ QuestionId anlegt, der als Schlüssel für die
Datenbankeinträge verwendet wird.
Aufgabe 2. Legen Sie die folgenden Routen für das Spielen des Quizes an:
• /quiz mit dem Handler QuizStartR und der HTTP-Methode GET
• /quiz/#QuestionId mit dem Handler QuizR und den HTTP-Methoden GET und POST
Die einzelnen Handler der Routen bewerkstelligen dabei:
• Das Aufrufen der Seite http: // localhost: 3000/ quiz (mit der GET-Methode) soll die
Startseite des Quizspiels darstellen und neben den Spielregeln einen Link zum Starten des
Quiz bieten. Eine mögliche Ansicht dieser Seite zeigt die folgende Abbildung:
18
Der Link verweist dabei auf die URL http://localhost:3000/quiz/i, wobei i eine zufällig aus dem Pool der Fragen ausgewählte QuestionId (als Zahl) ist.
Für das zufällige Auswählen eignet sich u.a. die Funktion randomRIO aus dem Modul
System.Random.
• Der Abruf einer URL http://localhost:3000/quiz/i (wobei i eine Zahl ist, die von
Yesod automatisch als QuestionId interpretiert wird) mit der GET-Methode, zeigt die
entsprechende Frage und ein Formular mit den vier Antwortmöglichkeiten an, wobei
die Antworten als Buttons realisiert sind, sodass beim Drücken des Buttons die Seite
http://localhost:3000/quiz/i mit der POST-Methode aufgerufen wird.
Eine mögliche Ansicht der Seite zeigt die folgende Abbildung:
• Der Abruf einer URL http://localhost:3000/quiz/i mit der POST-Methode zeigt
eine Seite an, die angibt, ob die gegebene Antwort richtig oder falsch war, den aktuellen Punktestand, sowie einen Link zur nächsten Frage. War die beantwortete Frage
die zehnte (und damit letzte Frage), so soll ein Link zum Neubeginn des Quizes (auf
http://localhost:3000/quiz) erscheinen. Mögliche Ansichten der Fälle (falsche Antwort, richtige Antwort, letzte Frage) zeigen die folgenden Abbildungen:
19
Die gegebene Antwort muss dabei aus den mit der POST-Methode übergebenen Daten ausgelesen werden. Das Session-Cookie soll verwendet werden, um folgende Informationen zu
speichern:
– der Punktestand (dieser muss ausgelesen und aktualisiert werden),
– die bereits gestellten Fragen (eine Liste der QuestionIds), damit keine Fragen doppelt
gestellt werden,
– die Anzahl der gestellten Fragen (hierfür kann die Länge der Liste der gestellten Fragen
verwendet werden).
Zur Auswahl der nächsten Frage, dürfen nur solche Fragen berücksichtigt werden, die noch
nicht gestellt wurden und unter den verbleibenden Fragen soll die Auswahl zufällig geschehen.
Wurde die letzte Frage gestellt, so sind die Daten für den Punktestand und die gestellten Fragen im Session-Cookie zu löschen (bzw. auf 0 und leere Liste zu setzen), damit der Neubeginn
wie gewünscht funktioniert.
Aufgabe 3. Legen Sie die folgenden Routen für die Administration der Quizfragen an:
• /admin mit dem Handler AdminShowR und der Methode GET
• /admin/new mit dem Handler AdminNewR und den Methoden GET und POST
• /admin/delete/#QuestionId mit dem Handler AdminDeleteR und den Methoden GET
und POST
• /admin/edit/#QuestionId mit dem Handler AdminEditR und den Methoden GET und
POST
Die einzelnen Handler der Routen bewerkstelligen dabei:
20
• Das Aufrufen der Seite http: // localhost: 3000/ admin (mit der GET-Methode) zeigt
eine Seite an, die:
– einen Link anbietet eine neue Frage einzugeben. Der Link zeigt auf den AdminNewRHandler
– eine Liste aller Fragen mit ihren Antworten (so dass die richtige Antwort erkennbar ist) anzeigt, wobei für jede Frage Links zum Bearbeiten (der Link zeigt
auf AdminEditR QuestionId) und zum Löschen der Frage (der Link zeigt auf
AdminDeleteR QuestionId) anbietet.
Eine mögliche Ansicht der Seite zeigt die folgende Abbildung:
• Das Aufrufen der Seite http: // localhost: 3000/ admin/ new mit der GET-Methode bietet ein Formular zum Eintragen einer neuen Frage. Beim Absenden des Formulars wird die
Route AdminNewR mit der POST-Methode aufgerufen, um die Formulardaten zu übermitteln.
Eine mögliche Ansicht der Seite zeigt die folgende Abbildung:
Beim Aufruf mit der POSTMethode, werden die Formulardaten ermittelt und die neue Frage
wird in die Datenbank eingetragen. Danach wird automatisch weitergeleitet auf die Route
AdminShowR (hierfür kann die Funktion redirect verwendet werden).
21
• Das Aufrufen der Seite http://localhost:3000/admin/delete/i mit der GET-Methode
zeigt die Frage mit der QuestionId samt der Antworten an und fragt, ob die Frage wirklich
gelöscht werden soll. Dafür gibt es ein Formular mit zwei Buttons mit „Ja“ und „Nein“.
Das Absenden des Formular ruft die gleiche Seite mit der POST-Methode auf.
Eine mögliche Ansicht der Seite zeigt die folgende Abbildung:
Bei Aufruf der Seite http://localhost:3000/admin/delete/i mit der POST-Methode
werden die mittels POST übermittelten Daten ausgelesen. Jenachdem welcher Button („Ja“
oder „Nein“) gedrückt wurde, wird die Frage mit QuestionId i aus der Datenbank gelöscht
oder auch nicht. Im Anschluss daran wird automatisch auf die Route AdminShowR weitergeleitet.
• Ein GET-Aufruf der Seite http://localhost:3000/admin/edit/i liefert ein Formular
zum Editieren der Frage mit der QuestionId i. Dabei werden die aktuellen Inhalte der Frage aus der Datenbank ausgelesen und dargestellt. Das Formular bietet einen Button zum
Absenden, der auf die selbe Seite mit der POST-Methode verweist. Eine mögliche Ansicht der
Seite zeigt die folgende Abbildung:
Bei Aufruf der Seite mit der POST-Methode wird der Datenbankeintrag der Frage aktualisiert
und anschließend auf die Route AdminShowR automatisch weitergeleitet.
22
Aufgabe 4. Die Route HomeR für das Wurzelverzeichnis / wird von Yesod automatisch angelegt.
Ändern Sie die Implementierung von getHomeR im Modul Handler.Home derart ab, dass eine
Startseite gezeigt wird, die zwei Links enthält:
Einen Link zum Starten des Quiz (verlinkt auf QuizStartR) und einen Link zur Administrationsoberfläche (verlinkt auf AdminShowR). Eine mögliche Ansicht der Seite zeigt die folgende Abbildung:
23
Herunterladen