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&szlig;buchstaben: #{T.toUpper txt} <p> Ein neuer Absatz. Dieser Text ist im gleichen Absatz Dieser Text nicht mehr, da Hamlet Einr&uuml;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