Servant - AG Programmiersprachen und Übersetzerkonstruktion

Werbung
Arbeitsgruppe für Programmiersprachen und Übersetzerkonstruktion
Institut für Informatik
Christian-Albrechts-Universität zu Kiel
Seminararbeit
Typsichere Web-Programmierung mit
Servant
Lasse Kristopher Meyer
Wintersemester 2015/2016
Betreut durch M. Sc. Sandra Dylus
ii
Inhaltsverzeichnis
Inhaltsverzeichnis
Inhaltsverzeichnis
ii
1. Einleitung
1
2. Grundlagen
2.1. Relevante Haskell-Erweiterungen . . . . . . .
2.1.1. Kind-Polymorphismus und Promotion
2.1.2. Typfamilien und Typoperatoren . . . .
2.2. Das Expression-Problem . . . . . . . . . . . .
2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2
2
5
6
3. Konzepte von Servant
7
4. Einführendes Beispiel
9
5. Umsetzung der Konzepte
5.1. Die DSL für Web-APIs . . . . . . . . . . . . .
5.1.1. Konstrukte und Syntax . . . . . . . . .
5.1.2. Implementierung der Konstrukte . . .
5.2. API-Interpretationen . . . . . . . . . . . . . .
5.2.1. Implementierung von Interpretationen
5.2.2. Verfügbare Interpretationen . . . . . .
11
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
11
11
12
13
13
18
6. Verwendungsbeispiel
20
7. Zusammenfassung und Diskussion
27
A. Vollständiges Beispiel zur Implementierung einer Client-Interpretation
28
B. Vollständiges Beispiel zur Implementierung eines Webservers
31
Literaturverzeichnis
36
iii
Abbildungsverzeichnis/Tabellenverzeichnis/Listings
Abbildungsverzeichnis
5.1. Ausschnitt der DSL-Grammatik . . . . . . . . . . . . . . . . . . . . . . . . . 12
6.1. Implementierung eines Webservers (HTML-Schnittstelle im Browser) . . . . 26
Listings
4.1. Einführendes Beispiel
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
5.1.
5.2.
5.3.
5.4.
5.5.
5.6.
5.7.
Implementierung einiger DSL-Konstrukte . . . . . . . . . . . . . . . . . . . .
Implementierung einer Client-Interpretation (Schnittstelle für Requests) . .
Implementierung einer Client-Interpretation (Typklasse HasSimpleCLient)
Implementierung einer Client-Interpretation (Instanz für :<|>) . . . . . . .
Implementierung einer Client-Interpretation (Instanz für symbols) . . . . .
Implementierung einer Client-Interpretation (Instanz für ReqBody) . . . . .
Implementierung einer Client-Interpretation (Instanz für Post) . . . . . . .
12
14
14
15
15
15
16
6.1.
6.2.
6.3.
6.4.
Implementierung eines Webservers (Beschreibung der API mit Servant)
Implementierung eines Webservers (Server-Logik, Teil 1) . . . . . . . .
Implementierung eines Webservers (Server-Logik, Teil 2) . . . . . . . .
Implementierung eines Webservers (HTML-Schnittstelle für /books) .
21
22
22
25
.
.
.
.
.
.
.
.
.
.
.
.
1 Einleitung
1
1. Einleitung
Der Entwurf und die Implementierung von webbasierten Anwendungen gehören zu den
häufigsten Aufgaben vieler Entwicklerinnen und Entwickler. Für diese Zwecke wird mittlerweile auch auf Programmiersprachen bzw. Programmierparadigmen zurückgegriffen, die in
diesem Bereich bisher eine eher untergeordnete Rolle gespielt haben. Insbesondere Konzepte aus der funktionalen Programmierung gewinnen zunehmend an Relevanz [Mor14],
etwa in Form multiparadigmatischer Programmiersprachen wie Scala [OSV08]. Die Vorteile
funktionaler Programmierung liegen dabei vor allem im hohen Abstraktionsgrad, in der
hohen Kompositionalität und in der Robustheit bzw. geringeren Fehleranfälligkeit, die aus
der Zustandslosigkeit funktionaler Programme und einer in der Regel starken Typisierung
folgt, durch die viele Fehler schon zur Übersetzungszeit erkannt werden können.
Auch die rein funktionale Programmiersprache Haskell [M+ 10] wird mittlerweile in vielen
Bereichen der Web-Programmierung erfolgreich eingesetzt [Has15]. Dies resultiert wohl
nicht zuletzt aus der hohen Anzahl an High-Level-Bibliotheken und -Frameworks, welche
die typsichere Entwicklung von Webdiensten und Client-Server-Architekturen ermöglichen.
Entsprechende Frameworks erlauben dabei typischerweise eine prägnante Beschreibung
der einzelnen Komponenten einer Webanwendung. Meist liegt der Fokus dabei auf dem
Server (wie z. B. bei Happstack [Sha]), da für den Client üblicherweise eine Kombination
aus HTML, CSS und JavaScript verwendet wird. Andere Frameworks wie Haste.app [EC14]
identifizieren hingegen den Client als zentrale Komponente des Client-Server-Modells.
In dieser Arbeit soll mit Servant [MHAL15] ein Haskell-Framework vorgestellt werden,
welches den Fokus auf die Schnittstelle zwischen Client und Server legt: die Web-API.
Servant erlaubt die Beschreibung von (HTTP-)Web-APIs in einer speziellen, erweiterbaren
domänenspezifischen Sprache (DSL) und ermöglicht die Definition von Funktionen auf
solchen API-Beschreibungen, um bspw. Dokumentation, Server-Infrastruktur oder ClientFunktionen daraus abzuleiten. Servant nutzt dabei in besonderem Maße die Möglichkeiten
von Haskells (erweitertem) Typsystem und dessen Ausdrucksstärke aus, was in dieser Arbeit
besonders berücksichtigt werden soll. Aus diesem Grund werden in Kapitel 2 zunächst
entsprechende Grundlagen, wie z. B. relevante Erweiterungen von Haskells Typsystem,
erläutert. Kapitel 3 skizziert daraufhin die zugrunde liegenden Konzepte von Servant. Nach
einem einführenden Beispiel in Kapitel 4 wird in Kapitel 5 anschließend die Umsetzung
dieser Konzepte in Haskell erläutert. In Kapitel 6 wird die Verwendung von Servant noch
einmal ausführlich an einem komplexeren Beispiel demonstriert, bevor sich Kapitel 7 der
Zusammenfassung und Diskussion widmet.
2 Grundlagen
2
2. Grundlagen
Ziel dieses Kapitels ist die Vermittlung der zum vollständigen Verständnis dieser Arbeit
notwendigen Grundlagen. Grundsätzlich setzt diese Arbeit beim Leser Basiswissen über
(HTTP-)Web-APIs bzw. Webdienste und typische Client-Server-Architekturen sowie elementare bis fortgeschrittene Kenntnisse der Eigenschaften und Konzepte der Programmiersprache Haskell voraus. Auf letzterem aufbauend werden im ersten Abschnitt dieses
Kapitels zunächst die für die Implementierung und Verwendung von Servant relevanten
Haskell-Spracherweiterungen1 (insbesondere Erweiterungen des Typsystems) anhand von
Beispielen beschrieben. Anschließend erfolgt eine Einführung in das sogenannte ExpressionProblem, welches sich mit der Ausdrucksstärke von Programmiersprachen im Allgemeinen
befasst. Wie wir sehen werden, stellt die Implementierung von Servant eine mögliche
Lösung dieses Problems für die Programmiersprache Haskell dar.
2.1. Relevante Haskell-Erweiterungen
2.1.1. Kind-Polymorphismus und Promotion
Als Kind (englisch für Art, Sorte) bezeichnet man in der Typentheorie den Typ eines
Typkonstruktors. Analog zum Typsystem für Ausdrücke in der Wert-Ebene werden durch
ein Kind-System die Wertebereiche für Ausdrücke in der Typ-Ebene festgelegt, so dass jeder
Typ-Ausdruck eindeutig einem Kind zugeordnet ist. Im Fall von Haskell ist das Kind-System
und die Menge K der Kinds standardmäßig wie folgt definiert (siehe [M+ 10], 4.1.1):
(1) ∗ ∈ K, wobei ∗ der Typ aller Datentypen ist (d. h. aller nicht-polymorphen, nullstelligen
Typkonstruktoren).
(2) κ1 , κ2 ∈ K ⇒ κ1 → κ2 ∈ K, wobei κ1 → κ2 der Typ aller polymorphen, unären Typkonstruktoren ist, welche einen Typ des Kinds κ1 erwarten und einen Typ des Kinds κ2
erzeugen.
Da nur Datentypen bzw. nullstellige Typkonstruktoren Werte besitzen, haben alle Werte
Typen des Kinds *. Folglich sind z. B. die Typen Bool und [Int] vom Kind * – das selbe gilt
aber auch für Funktionstypen wie (String -> Int) -> Bool. Unäre Typkonstruktoren
wie Maybe oder [] sind hingegen vom Kind * -> *, während Kinds von Typkonstruktoren
höherer Stelligkeit mittels Currying gebildet werden. So ist bspw. der Typ Either vom Kind
* -> * -> *, was dem Kind * -> (* -> *) entspricht.
1 Genauer:
Spracherweiterungen für die Implementierung des Haskell-2010-Standards durch den Glasgow
Haskell Compiler (GHC), welche im Folgenden implizit gemeint ist, wenn von „Haskell“ die Rede ist.
2 Grundlagen
3
Ähnlich wie bei Wert-Ausdrücken prüft Haskell die Gültigkeit von Typ-Ausdrücken gegen das Kind-System durch einen Inferenzmechanismus. Während allerdings parametrischer Polymorphismus mittels Typvariablen in der Typ-Ebene unterstützt wird, ist KindPolymorphismus aufgrund des impliziten Charakters des Kind-Systems und den Regeln
des Kind-Inferenzsystems standardmäßig nicht möglich. So inferiert Haskell für Typvariablen immer den Kind *, falls die zugehörige Typdefinition nicht explizit die Stelligkeit des
Typkonstruktors festlegt (siehe [M+ 10], 4.6). Bspw. erzeugen folgende Definitionen einen
Typfehler, da der für die Typvariable a inferierte Kind * nicht mit dem Kind * -> * des
Typkonstruktors Maybe übereinstimmt, obwohl a offensichtlich Kind-polymorph ist:
data SillyList a = Empty | Cons (SillyList a)
type VerySillyList = SillyList Maybe -- rejected by default
Kind-Polymorphismus wird durch die Erweiterung PolyKinds ermöglicht, mit der für den
Typkonstruktor SillyList nun der Kind k -> * inferiert wird, wodurch a nun auch vom
Kind * -> * sein darf. Analog zu Typsignaturen in der Wert-Ebene ist es mit PolyKinds
zudem möglich, Kind-Polymorphismus über Kind-Annotationen bzw. Kind-Signaturen2
einzuschränken:
{-# LANGUAGE PolyKinds #-}
data SillyList (a :: * -> *) = Empty | Cons (SillyList a)
Haskell inferiert für SillyList nun (* -> *) -> *, so dass bspw. SillyList Int bzgl.
des Kind-Systems kein gültiger Typ mehr ist.
Kind-Signaturen spielen insbesondere auch bei Generalized Algebraic Datatypes (GADTs)
(und Typfamilien, siehe Abschnitt 2.1.2) eine Rolle. GADTs [PJVWW06] verallgemeinern
algebraische Datentypen, indem sie die explizite Angabe der Konstruktortypen erlauben,
wobei etwaige Typparameter des Typkonstruktors im Ergebnistyp der Konstruktoren gemäß
des Kinds instanziiert werden dürfen:
{-# LANGUAGE GADTs,PolyKinds #-}
data MyTrue
data MyFalse
data List :: * -> * -> * where
Empty :: List a MyTrue
Cons :: a -> List a b -> List a MyFalse
Obiges Beispiel repräsentiert einen Listentyp, bei dem die Information darüber, ob die Liste
leer ist, explizit im Typ enthalten ist. MyTrue und MyFalse werden dabei in Form von leeren
Datentypen definiert, da sie Werte in der Typ-Ebene repräsentieren. Sinnvollerweise sollte
der zweite Typparameter von List nun nur durch die Typen MyTrue und MyFalse instanziiert werden können. Allerdings ist auch List Int Bool gemäß der Kind-Signatur (* -> *
-> *) ein gültiger Typ, was durch das sehr simple Kind-System zunächst nicht zu verhindern
ist, da für nullstellige Typkonstruktoren kein einschränkenderer Kind als * zur Verfügung
steht. Insbesondere sobald Berechnungen in der Typ-Ebene vorgenommen werden, ist
das Kind-System somit standardmäßig häufig nicht einschränkend genug. Um dieses Problem zu beheben, ermöglicht die Erweiterung DataKinds das „Heben“ von Datentypen in
die Kind-Ebene, auch (Datatype) Promotion [YWC+ 12] genannt. DataKinds ermöglicht
2 Eigentlich
werden Kind-Annotationen bzw. Kind-Signaturen durch die Erweiterung KindSignatures ermöglicht, welche aber von PolyKinds impliziert wird.
2 Grundlagen
4
somit die Definition von Kinds über die Definition von herkömmlichen Datentypen:
{-# LANGUAGE DataKinds,GADTs,PolyKinds #-}
data MyBool = MyTrue | MyFalse -- also defines the kind ‘MyBool‘
data List :: * -> MyBool -> * where
Empty :: List a MyTrue
Cons :: a -> List a b -> List a MyFalse
Zusätzlich zum Datentyp MyBool wird jetzt der Kind MyBool definiert, sowie zwei (nullstellige) Typkonstruktoren MyTrue und MyFalse vom Kind MyBool. Durch das Anpassen
der Kind-Signatur von List im zweiten Typparameter können so dessen gültige Instanzen auf MyTrue und MyFalse eingeschränkt werden, d. h. List Int Bool ist gemäß der
Kind-Signatur kein gültiger Typ mehr. Weiterhin erlaubt DataKinds Promotion auch für
parametrisierte Datentypen und ermöglicht so die Verwendung von (polymorphen) Listen
in der Typ-Ebene mit der gewohnten Syntax:
data ListOfLists :: * -> [MyBool] -> * where
Empty2 :: ListOfLists a ’[]
Cons2 :: List a b -> ListOfLists a bs -> ListOfLists a (’(:) b bs)
Der Datentyp ListOfLists bs sammelt zu jeder enthaltenen List a b im Parameter bs
(einer Liste von Typen des Kinds MyBool) in der Typ-Ebene die Information, ob sie leer ist
oder nicht. So gilt bspw:
(Cons2 Empty (Cons2 (Cons 42 Empty) Empty2)) :: ListOfLists Int ’[’MyTrue,’MyFalse]
Das vorangestellte einfache Anführungszeichen kennzeichnet hierbei durch Promotion in
die Typ-Ebene gehobene Datenkonstruktoren, die in Haskell ja potentiell den gleichen
Namen haben dürfen wie Typkonstruktoren.
Über das GHC-Modul GHC.TypeLits hat man zu guter Letzt Zugriff auf in die Typ-Ebene
gehobene Zahl- und String-Literale vom Kind Nat bzw. Symbol:
{-# LANGUAGE DataKinds,PolyKinds #-}
import GHC.TypeLits
type Number = 42
-- has kind ‘Nat‘
type Message = "Hello World!" -- has kind ‘Symbol‘
Zusätzlich definiert GHC.TypeLits die Typklassen KnownNat und KnownSymbol, die von
jedem konkreten Nat- bzw- Symbol-Literal instanziiert werden und eine Umwandlung in
die entsprechenden Integer- bzw. String-Werte ermöglichen:
data Proxy (a :: k) = Proxy
fortyTwoInteger = natVal
(Proxy :: Proxy Number ) -- has type ‘Integer‘
helloWorldString = symbolVal (Proxy :: Proxy Message) -- has type ‘String‘
Der Kind-polymorphe, konkrete Datentyp Proxy wird benötigt, um für die leeren LiteralDatentypen einen Wert erzeugen zu können, der dann wiederum an die Funktionen
natVal und symbolVal übergeben werden kann. symbolVal hat bspw. den Typ forall n
proxy. KnownSymbol n => proxy n -> String und berechnet im obigen Beispiel für
den Typ Message bzw. "Hello World!" entsprechend den String "Hello World!" über
die KnownSymbol-Instanz des Literals. Der Typkonstruktor Proxy ist dabei typischerweise
im ersten Argument Kind-polymorph, um für leere Typen beliebiger Kinds Werte erzeugen
zu können.
2 Grundlagen
5
2.1.2. Typfamilien und Typoperatoren
Typfamilien sind parametrisierte Typen, denen man abhängig von der konkreten Instanziierung der Typparameter verschiedene Definitionen zuweisen kann. Eine Typfamilie ist
entweder eine Datentyp-Familie oder eine Typ-Synonym-Familie. Datentyp-Familien entsprechen indizierten data- bzw. newtype-Definitionen und ermöglichen das Überladen
von Werten. Typ-Synonym-Familien entsprechen indizierten type-Definitionen und können auch als Typ-Funktionen interpretiert werden [SPJCS08]. In beiden Fällen kann die
Definition „freistehend“ auf Top-Level oder innerhalb einer Typklasse erfolgen. Für die
Verwendung und Implementierung von Servant sind insbesondere Typ-Synonym-Familien
von Bedeutung, weshalb Datentyp-Familien im Folgenden nicht weiter betrachtet werden.
Freistehende Typ-Synonym-Familien werden mit dem zusätzlichen Schlüsselwort family
gekennzeichnet:
{-# LANGUAGE DataKinds,PolyKinds,TypeFamilies #-}
data Nat = Zero | Succ Nat
type family Length (ts :: [k]) :: Nat where
Length ’[]
= Zero
Length (’(:) t ts) = Succ (Length ts)
Die geschlossene, mit dem Typparameter ts indizierte Typfamilie (bzw. Typ-Funktion)
Length entspricht hier der length-Funktion in der Typ-Ebene, so dass bspw. der Typ
Length ’[Int,Bool] ein Typ-Synonym für ’Succ (’Succ ’Zero) ist. Offene Typfamilien werden ohne den where-Block deklariert und können potenziell über mehrere Module verteilt mittels Typ-Instanzen (type instance) definiert bzw. erweitert werden.
Typ-Familien innerhalb von Klassendefinitionen sind hingegen immer offen und müssen
nicht mit dem Schlüsselwort family gekennzeichnet werden:
{-# LANGUAGE TypeFamilies #-}
class EncapsulateDatatype a where
type Type a :: *
Die Deklaration von Typ-Instanzen erfolgt zusammen mit der Instanziierung der Klasse für
einen konkreten Typ a:
instance EncapsulateDatatype (Maybe a) where
type Type (Maybe a) = a
Für eine Instanz der Typklasse EncapsulateType des Typs t berechnet die Typ-Funktion
Type somit den in t gekapselten Typ.
Der Vollständigkeit halber sei abschließend noch die Erweiterung TypeOperators erwähnt,
die die Definition von Infix-Typoperatoren erlaubt:
{-# LANGUAGE TypeOperators #-}
data a :~: b = Foo a b
infixr 8 :~:
Mit TypeOperators kann bspw. auch der durch Promotion in die Typ-Ebene gehobene
Typkonstruktor ’(:) in gewohnter Schreibweise verwendet werden (Int ’: ’[]).
2 Grundlagen
6
2.2. Das Expression-Problem
Der Begriff Expression Problem wurde von Philip Wadler eingeführt [Wad98] und beschreibt das Problem der zweidimensionalen Erweiterbarkeit von Programmen bzgl. der
verwendeten Datentypen und den Funktionen auf diesen Datentypen – insbesondere im
Kontext stark getypter Programmiersprachen. Ziel ist es, Datentypen über ihre Konstruktoren so definieren zu können, dass zu einem späteren Zeitpunkt sowohl neue Konstruktoren
als auch Funktionen über diese Datentypen hinzugefügt werden können, ohne dabei
existierenden Code neu übersetzen zu müssen oder Typsicherheit zu verlieren.
In statisch getypten, funktionalen Programmiersprachen wie Haskell ist es typischerweise
einfach, für einen gegebenen Datentyp neue Funktionen hinzuzufügen, während die
Datentyp-Definition geschlossen und somit nicht erweiterbar ist. In objektorientierten
Sprachen wie Java ist es hingegen leicht möglich, Datentypen durch abgeleitete Klassen zu
erweitern, allerdings können existente Klassen nicht einfach um Methoden ergänzt werden.
Die Fähigkeit einer Programmiersprache das Expression-Problem lösen zu können, ist somit
ein Indikator für die Ausdrucksstärke dieser Sprache.
Für viele Sprachen existieren spezielle Lösungsvorschläge für das Expression-Problem, die
die gleichzeitige Erweiterbarkeit von Datentypen und Funktionen ermöglichen. Im Falle
von Haskell beschreiben bspw. Löh und Hinze eine (Syntax-)Erweiterung, die die explizite
Definition von offenen Datentypen bzw. Funktionen ermöglichen soll [LH06]:
open data Expr :: *
Num :: Int -> Expr
Plus :: Expr -> Expr -> Expr
open eval :: Expr -> Int
eval (Num n)
= n
eval (Plus e1 e2) = eval e1 + eval e2
Durch die Verwendung des Schlüsselworts open werden der Datentyp Expr und die Funktion eval als offen deklariert, d.h. dass Expr und eval in anderen Programmteilen (z. B. in
einem anderen Modul) um weitere Konstruktoren bzw. Funktionsregeln ergänzt werden
können:
Mult :: Expr -> Expr -> Expr
-- extends ‘Expr‘
eval (Mult e1 e2) = eval e1 * eval e2 -- extends ‘eval‘
Wie bereits einleitend erwähnt wurde, skizziert die Implementierung von Servant eine
Lösung des Expression-Problems für Haskell. Im Gegensatz zu dem Vorschlag von Löh und
Hinze basiert diese jedoch im Wesentlichen auf Haskells Typklassen und den in Abschnitt
2 beschriebenen Spracherweiterungen. Tatsächlich ist die durch das Expression-Problem
beschriebene Erweiterbarkeit sogar eine der Richtlinien, die Servant zugrunde liegen. Diese
Richtlinien sowie die Kernkonzepte von Servant sollen nun im nächsten Kapitel näher
betrachtet werden.
3 Konzepte von Servant
7
3. Konzepte von Servant
Servant ist eine Haskell-Bibliothek (genau genommen eine Menge von Haskell Bibliotheken), die im Wesentlichen den Kern einer erweiterbaren domänenspezifischen Sprache
für die Beschreibung von Web-APIs – also Schnittstellen von Webdiensten bzw. Webservern – bereitstellt. Wie eingangs erwähnt, liegt bei Servant der Schwerpunkt also nicht
auf der Bereitstellung einer speziellen, festgelegten Schnittstelle für die Programmierung
von Server- oder Client-Software. Vielmehr ist es mit Servant möglich, die mit der DSL
formulierten APIs auf beliebige Weise zu interpretieren und ihnen so eine bestimmte Semantik zu geben. Bspw. ist eine Web-API immer auch Teil der Dokumentation des zugrunde
liegenden Webservers – eine entsprechende Interpretation könnte nun Funktionen zur
Verfügung stellen, um aus einer beliebigen API-Beschreibung durch die DSL systematisch
die textuelle Beschreibung der Endpunkte des Webservers mit ihren Constraints abzuleiten.
Interpretationen sind dabei so mächtig und flexibel, dass sie z. B. auch genutzt werden
können, um den Boilerplate-Code für die Implementierung konkreter Server(-Handler) aus
API-Beschreibungen abzuleiten, oder um systematisch typsichere Client-Funktionen für ein
entsprechendes Server-Interface zu generieren. Mittels Interpretationen kann Servant also
auch als Infrastruktur für die Implementierung konkreter Webanwendungen genutzt werden. Neben der DSL für API-Beschreibungen sind Interpretationen das zweite Kernkonzept
von Servant.
Laut den Autoren basiert der Entwurf und die Umsetzung von Servant weiterhin auf
folgenden Richtlinien (siehe [MHAL15], Homepage):
(1) Präzision, Ausdrucksstärke Die Formulierung von APIs soll einer simplen aber fle-
xiblen Syntax folgen und die Anreicherung mit möglichst vielen (Meta-)Informationen
(z. B. Constraints) erlauben, so dass Interpretationen alle nötigen Informationen einzig aus der Beschreibung der konkreten API beziehen können.
(2) Wiederverwendbarkeit, Abstraktion Servant soll durch ein hohes Maß an Abstraktion
Wiederverwendbarkeit gewährleisten. Sei es bei der Spezifikation der API durch die
DSL (wenn z. B. mehrere Endpunkte eines Dienstes den selben Constraints unterliegen
soll es möglich sein, diese ein einziges Mal zu definieren und an den entsprechenden
Stellen wiederzuverwenden) oder bei der Interpretation von APIs (z. B. bei der
Interpretation einer API durch mehrere Server-Handler, die sich Logik teilen).
(3) Typsicherheit Der Compiler soll statisch feststellen können, ob die aus einer Interpre-
tation abgeleiteten Programm-Komponenten zu einer bestimmten API-Beschreibung
passen (z. B. ob der Rückgabetyp eines Server-Handlers für einen Endpunkt mit dem
durch die API-Beschreibung spezifizierten Typ für die zugehörige Response übereinstimmt).
3 Konzepte von Servant
8
(4) Flexibilität, Erweiterbarkeit Die API-DSL und der Mechanismus für die Erstellung von
Interpretationen sind ausdrücklich offen, d. h. es soll dem Anwender möglich sein, je
nach Anwendungsfall und Bedarf entweder die DSL selbst zu erweitern oder eigene
Interpretationen zu definieren.
Offensichtlich entspricht die im letzten Punkt geforderte zweidimensionale Erweiterbarkeit
von DSL und Interpretationen einer speziellen Ausprägung des Expression-Problems (siehe
Abschnitt 2.2), wobei die DSL die Rolle des Datentyps einnimmt und die Interpretationen
den Funktionen auf diesem Datentyp entsprechen.
Der Schlüssel für die Umsetzung der Kernkonzepte und Prinzipien von Servant ist einerseits
Haskell als rein funktionale Programmiersprache im Allgemeinen und andererseits Haskells
Typsystem mit den in Abschnitt 2.1 beschriebenen Erweiterungen im Speziellen. So kommt
der funktionale Charakter und der daraus folgende hohe Abstraktionsgrad von Haskell
natürlich Punkt (2) zugute. Für die Realisierung der in Punkt (3) geforderten Typsicherheit
nutzt Servant in besonderem Maße Haskells Typsystem aus. So werden in Servant APIs
durch Typen repräsentiert, d. h. aus der DSL abgeleitete Wörter (API-Beschreibungen)
sind mittels Typoperatoren verknüpfte Typ-Ausdrücke. Interpretationen entsprechen hingegen Typklassen, die für die Operatoren und elementaren Konstrukte der DSL instanziiert
werden und so die Definition von Funktionen auf beliebigen, gemäß der DSL-Grammatik
zusammengesetzten API-Beschreibungen ermöglichen.
9
4 Einführendes Beispiel
4. Einführendes Beispiel
Bevor im folgenden Kapitel die konkrete Umsetzung der Konzepte (API-DSL und Interpretationen) und Design-Entscheidungen (APIs als Typen bzw. Interpretation als Typklassen) in
Haskell beschrieben wird, soll zunächst ein minimales Beispiel die Verwendung von Servant
demonstrieren und die bisherigen Ausführungen „greifbarer“ machen. Ein komplexeres
Verwendungsbeispiel wird dann in Kapitel 6 beschrieben.
Wir betrachten für dieses sehr einfache Beispiel einen (zustandslosen) Webserver mit einem einzigen Endpunkt /hello/world, der über einen HTTP-GET-Request angesprochen
werden kann und als Response den Klartext Hello World! zurücksendet. In der Dokumentation des Webservers würde man diesen Endpunkt vielleicht wie folgt beschreiben:
GET /hello/world
sends a friendly greeting to the client
In Servant werden APIs durch Typen repräsentiert. Das spezifizierte Interface würde in
Servant folgendem Typ entsprechen:
type HelloWorldAPI = "hello" :> "world" :> Get ’[PlainText] Text
Der Typkonstruktor Get ist Teil der DSL und beschreibt offenbar die erwartete RequestMethode. Die Typ-Liste im ersten Parameter enthält die vom Server für diesen Endpunkt
unterstützten Content Types (hier nur Klartext) und der zweite Parameter legt den Typ
fest, den die Antwort in der Haskell-Welt hat. "hello" und "world" sind Typ-Literale, die
den Pfad repräsentieren. Über den Typoperator (:>) (ebenfalls Teil der DSL) werden die
einzelnen Komponenten des Endpunkts verknüpft. Wir wollen unseren Endpunkt nun als
Server interpretieren und als solchen implementieren. Eine entsprechende Interpretation
wird durch das Paket servant-server1 bereitgestellt.
1
2
3
4
5
6
7
8
9
10
11
12
{-# LANGUAGE DataKinds,TypeOperators #-}
import Data.Text
(Text,pack)
import Servant
(Get,PlainText,Proxy(..),Server,(:>),serve)
import Network.Wai.Handler.Warp (run)
type HelloWorldAPI = "hello" :> "world" :> Get ’[PlainText] Text
helloWorldHandler :: Server HelloWorldAPI
helloWorldHandler = return $ pack "Hello World!"
main :: IO ()
main = run 8000 $ serve (Proxy :: Proxy HelloWorldAPI) helloWorldHandler
Listing 4.1: Einführendes Beispiel
1 http://hackage.haskell.org/package/servant-server
4 Einführendes Beispiel
10
Listing 4.1 zeigt, wie sich ein (lokaler) Server mit diesem Paket implementieren lässt.
servant-server definiert insbesondere die Server-Monade (Zeile 8) und die Funktion serve (Zeile 12). Ein Wert vom Typ Server api ist offenbar ein Handler für eine API, die durch den Typ api repräsentiert wird (hier HelloWorldAPI). Der Handler
helloWorldHandler in den Zeilen 8 und 9 macht in unserem Fall nichts anderes als
den String "Hello World!" in Form eines Text-Wertes in der Server-Monade zurückzugeben. Die Rückgabe des reinen String-Wertes würde an dieser Stelle einen Typfehler
erzeugen. Der Typ Server HelloWorldAPI sorgt also dafür, dass der Handler zur APISpezifikation konform sein muss. Die Funktion serve verwandelt den zu unserer API
konformen Handler in eine Application (aus der wai-Bibliothek), welche über die Funktion run (aus der warp-Bibliothek) als Webserver ausgeführt werden kann. Die Pakete
wai und warp sind dabei völlig unabhängig von Servant und bilden das Back-End von
servant-server. Neben dem Handler erwartet serve auch ein entsprechendes ProxyObjekt (siehe Abschnitt 2.1.1). Dies soll erst später begründet werden, weist allerdings
bereits darauf hin, dass es sich bei HelloWorldAPI um einen leeren Typ handelt.
Der Server kann nun über die main-Funktion gestartet werden und gibt für API-konforme
Anfragen erwartungsgemäß die Klartext-Nachricht aus:
curl -X GET localhost:8000/hello/world -w ’\n’
Hello World!
Für nicht API-konforme Anfragen generiert servant-server hingegen automatisch entsprechende Fehlernachrichten:
curl -X GET localhost:8000/does/not/exist -w ’\n’
not found
curl -X POST localhost:8000/hello/world -w ’\n’
method not allowed
5 Umsetzung der Konzepte
11
5. Umsetzung der Konzepte
5.1. Die DSL für Web-APIs
Die durch Servant definierte DSL erlaubt die Beschreibung von Web-APIs HTTP-basierter
Webserver. Solche Webserver bestehen typischerweise aus einer Menge von statischen, über
das Internet erreichbaren Endpunkten, die das HTTP-Request-Response-Nachrichtensystem
des Webservers bilden. HTTP-Requests bestehen aus einer Anfragemethode (GET, POST, PUT,
DELETE, usw.), dem relativen Pfad der entsprechenden Ressource auf dem Webserver, einem
Query-String, keinem oder mehreren Request-Headerfeldern und dem (möglicherweise
leeren) Inhalt der Anfrage. Vom Server generierte HTTP-Responses bestehen wiederum
aus einem dreistelligen Statuscode, keinem oder mehreren Response-Headerfeldern und
dem (möglicherweise leeren) Inhalt der Antwortnachricht. Web-APIs beschreiben nun die
Endpunkte eines Webservers zusammen mit ihren jeweiligen Constraints bzgl. des Formats
der erwarteten Requests (z. B. Pfad, Anfragemethode, erwartete Header und Inhalt der
Nachricht) bzw. generierten Responses (z. B. enthaltene Header und Inhalt der Nachricht).
5.1.1. Konstrukte und Syntax
Servant definiert entsprechende Konstrukte für die Beschreibung von Endpunkten als Sequenzen von Constraints. Abbildung 5.1 zeigt einen Ausschnitt der Syntax der API-DSL mit
den wichtigsten dieser Konstrukte. Mehrere Endpunkte einer Web-API können über den
Operator :<|> kombiniert werden. Mit dem Operator :> werden Sequenzen von RequestConstraints (items) für einen Endpunkt gebildet. Request-Constraints entsprechen genau
den oben beschriebenen Einschränkungen an das Format eines HTTP-Requests. Wie wir
schon in Beispiel 4.1 gesehen haben, wird der Pfad der Ressource auf dem Webserver durch
eine Sequenz von Typ-Ebenen-Strings (symbols) dargestellt. Variable Komponenten eines
solchen Pfades (wie z. B. :id in /products/:id) werden durch das Capture-Constraint
repräsentiert, wobei über den symbol-Parameter der Name der Variable festgelegt wird.
Der Parameter t spezifiziert stets den Typ eines Datums in der Haskell-Welt (bei Capture
ist dieses Datum der Wert der Variable). Das Header-Constraint verlangt wiederum ein bestimmtes Headerfeld im Request, wobei analog zu Capture über den symbol-Parameter der
Name des Headerfeldes und über t der Haskell-Typ des Werts spezifiziert wird. Auf ähnliche
Weise beschreibt ReqBody das Format des Inhalts der Anfrage. Die Typ-Liste ctypes legt
dabei fest, welche Content Types vom Server akzeptiert werden. Jede Constraint-Sequenz
endet schließlich mit einer Anfragemethode (method). Im einfachsten Fall besteht ein
Endpunkt nur aus einer solchen Methode, was dem Endpunkt / ohne weitere Constraints
entspricht. Über das method-Konstrukt kann auch das Format der HTTP-Response spezifiziert werden. Analog zu ReqBody können z. B. die möglichen Content Types der Antwort
12
5 Umsetzung der Konzepte
api
::= api :<|> api
|
item :> api
|
method
item ::=
|
|
|
|
symbol
header
ReqBody ctypes t
Capture symbol t
...
method ::=
|
|
|
rtype
Get ctypes rtype
Put ctypes rtype
Post ctypes rtype
...
headers
header
ctypes
ctype
::= Headers headers t
|
t
symbol
t
::=
::=
::=
::=
|
|
|
::=
::=
’[header, ...]
Header symbol t
’[ctype, ...]
PlainText
JSON
HTML
...
type-level str.
Haskell type
Abbildung 5.1.: Ausschnitt der DSL-Grammatik
festgelegt werden. rtype entspricht hier dem Typ der Antwort in der Haskell-Welt, wobei
dieser noch um etwaige Response-Header ergänzt werden kann. Die drei Punkte bei item,
method und ctype deuten an, dass es sich konzeptionell um offene Definitionen handelt,
die beliebig um neue Konstrukte erweitert werden können. Es ist weiterhin anzumerken,
dass in Abbildung 5.1 nicht alle Konstrukte enthalten sind, die Servant standardmäßig
bereitstellt. Bspw. gibt gibt es auch Request-Constraint-Konstrukte für die Beschreibung von
Query-Strings (QueryFlag, QueryParams, ...), die im Zuge dieser Arbeit aber nicht weiter
betrachtet werden.
5.1.2. Implementierung der Konstrukte
Statt für die einzelnen syntaktischen Kategorien der Grammatik (api, item, etc.) aus Abbildung 5.1 Datentypen mit entsprechenden Konstruktoren anzulegen und API-Beschreibungen
somit als Haskell-Werte darzustellen, realisiert Servant die DSL auf Typ-Ebene, d. h. jeder
Ausdruck der DSL ist ein Haskell-Typ. Dazu werden die einzelnen Konstrukte als Datentypen
implementiert. Listing 5.1 zeigt die konkrete Umsetzung einiger Konstrukte:
1
2
3
4
5
6
7
8
9
10
11
12
data api1 :<|> api2 = api1 :<|> api2
infixr 8 :<|>
data (item :: k) :> api
infixr 9 :>
data ReqBody (ctypes :: [*]) t
data Capture (symbol :: Symbol) t
data Get (ctypes :: [*]) t
data PlainText
Listing 5.1: Implementierung einiger DSL-Konstrukte
Mit Ausnahme des Typoperators :<|> handelt es sich hier ausschließlich um leere DatentypDefinitionen (auf den Grund für diese Ausnahme kommen wir in Abschnitt 5.2.1 zurück).
Somit sind konkrete API-Beschreibungens stets Haskell-Typen ohne Werte. Die entsprechende Semantik in der Wert-Ebene erhalten wir stattdessen über die erwähnten Interpretatio-
5 Umsetzung der Konzepte
13
nen (auch dies wird in Abschnitt 5.2 konkretisiert).
Da alle Konstrukte durch Datentypen repräsentiert werden, sind sie (trotz der Leerheit)
vom Kind *. Dies ist für die Umsetzung von Punkt (4) aus Kapitel 3 elementar, da es
sich bei * um einen offenen Kind handelt. Würden die einzelnen syntaktischen Kategorien
selbst definierten, geschlossenen Kinds entsprechen (siehe Abschnitt 2.1.1), so wäre zwar
eine größere Typsicherheit gewährleistet, es wäre aber nicht möglich zu einem späteren
Zeitpunkt neue DSL-Konstrukte zu definieren. Einzig Typ-Ebenen-Strings sind vom Kind
Symbol, weshalb bspw. das erste Argument von :> explizit Kind-polymorph sein muss. Zudem werden so bei Konstrukten wie Capture wieder einschränkendere Kind-Annotationen
ermöglicht (siehe Zeile 8).
Die Umsetzung der DSL auf Typ-Ebene bringt einige Vorteile (oder zumindest in mancher
Hinsicht keine Nachteile): Zunächst handelt es sich bei Typen in Haskell ebenfalls um
First-Class-Objekte, d. h. sie können (über type-Definitionen) benannt werden, sie können
Argument bzw. Rückgabewert von (Typ-)Funktionen sein, mit anderen Typen verknüpft
und von Modulen exportiert werden. Über das Kind-System wird eine grundlegende
Typsicherheit gewährleistet, es ist aber durch den Kind * flexibel genug, um dem Anspruch
der Erweiterbarkeit gerecht zu werden. Die Datentypen aus Listing 5.1 sind zudem völlig
unabhängig voneinander, d. h. sie können in unterschiedlichen Modulen oder Paketen
definiert werden, so dass neue Konstrukte hinzugefügt werden können, ohne Servant selbst
ändern zu müssen. Dies ist eine der Grundbedingungen bei der Lösung des ExpressionProblems (siehe Abschnitt 2.2). Gleichzeitig ist aber auch offensichtlich, dass abstrakte APIBeschreibungen in Form von leeren Typen für sich genommen noch keinen großen Mehrwert
bieten. Wie bereits erwähnt, entsteht dieser erst in Kombination mit Interpretationen, die
auch einen entscheidenden Beitrag zu der in Punkt (3) geforderten Typsicherheit leisten.
5.2. API-Interpretationen
5.2.1. Implementierung von Interpretationen
In diesem Abschnitt soll anhand eines Beispiels demonstriert werden, nach welchem
Schema API-Interpretationen implementiert werden und wie die in einer abstrakten APIBeschreibung durch Typen kodierten Informationen genutzt werden können, um daraus
konkrete, typkonforme Programm-Komponenten auf Wert-Ebene abzuleiten. Das Ziel ist die
Interpretation von API-Beschreibungen durch Client-Funktionen, die typsicher bezüglich der
API-Spezifikation sind und bspw. von der (De-)Serialisierung von Daten abstrahieren. Die
Interpretation soll uns ermöglichen, solche Funktionen systematisch aus einer beliebigen
API-Beschreibung abzuleiten.
Wir nehmen zunächst an, dass uns die durch Listing 5.2 gegebene Schnittstelle zur Verfügung steht, die aus dem Datentyp Req für vereinfachte HTTP-Requests (bestehend aus
Pfad, Methode, einem optionalen Accept-Header und einem optionalen Body), einem simplen Fehlertyp (ReqError) und einer Funktion performRequest besteht. MediaType und
RequestMethod sind Typen aus verschiedenen Network.HTTP-Modulen und repräsentieren Content Types bzw. Request-Methoden. performRequest erhält einen Request, sendet
5 Umsetzung der Konzepte
14
diesen (der Einfachheit halber) an die Basis-URL localhost:8000 und liefert entweder
einen ReqError oder (bei Erfolg) den Inhalt des Response-Bodys als ByteString. Intern
macht performRequest dabei natürlich Gebrauch von der IO-Monade. Die konkrete Implementierung von performRequest soll hier nicht weiter betrachtet werden – theoretisch
kann diese auf jeder geeigneten Haskell-Bibliothek für Netzwerkkommunikation basieren.
1
2
3
4
5
6
7
8
data Req = Req
{ path
:: String,
method :: RequestMethod
, accept :: Maybe MediaType, body
:: Maybe (ByteString,MediaType)
}
data ReqError = ReqError String deriving (Show)
performRequest :: Req -> EitherT ReqError IO ByteString
Listing 5.2: Implementierung einer Client-Interpretation (Schnittstelle für Requests)
Wie bereits erwähnt, werden API-Interpretationen über Typklassen realisiert. Mittels Instanziierung für bestimmte Konstrukte der API-DSL können so für beliebige Ausdrücke
über diesen Konstrukten Typ-Synonyme und Funktionen definiert werden. Solche TypSynonyme und Funktionen interpretieren dann abstrakte API-Beschreibungen. Für unsere
Client-Interpretation definieren wir die Typklasse HasSimpleClient wie folgt:
1
2
3
class HasSimpleClient api where
type SimpleClient api :: * -- declares an open type function!
simpleClientWithRoute :: Proxy api -> Req -> SimpleClient api
Listing 5.3: Implementierung einer Client-Interpretation (Typklasse HasSimpleCLient)
Die Idee ist die folgende: Die Typ-Funktion SimpleClient berechnet induktiv über den
Aufbau der API-Beschreibung api den Typ eines zur API konformen Clients. So soll bspw. für
einen Endpunkt der Form ReqBody ’[PlainText] Text :> Post ’[] () eine ClientFunktion generiert werden, die den Typ Text -> EitherT ReqError IO () hat. Für eine
API mit mehreren Endpunkten soll hingegen ein Tupel von jeweils Endpunkt-konformen
Client-Funktionen erzeugt werden, usw.. Die Methode simpleClientWithRoute berechnet
dann ebenfalls induktiv über den Aufbau der API die konkrete(n) Client-Funktion(en) in
der Wert-Ebene. Ein initial übergebener Standard-Req-Wert wird dabei entsprechend den
in der API kodierten Informationen vervollständigt. Wie die Funktion serve aus Listing
4.1 benötigen wir zusätzlich noch ein Proxy-Objekt, da SimpleClient (genau wie Server)
als Typ-Funktion potentiell nicht injektiv ist und somit von Haskell nicht zur Inferenz des
Typparameters api im Klassen-Constraint verwendet werden kann.
Im Folgenden wollen wir nun beispielhaft für einige DSL-Konstrukte HasSimpleClientInstanzen definieren. Wir beginnen mit dem Kombinator :<|> (Listing 5.4). In diesem Fall
erzeugen wir für die beiden API-Beschreibungen a1 und a2 über deren HasSimpleClientInstanzen rekursiv entsprechende Clients und kombinieren diese zu einer Alternative –
sowohl in der Typ-Ebene als auch in der Wert-Ebene mit dem Konstruktor :<|>. Aus diesem
Grund ist der entsprechende Datentyp auch nicht leer (siehe Abschnitt 5.1.2).
5 Umsetzung der Konzepte
1
2
3
4
5
15
instance (HasSimpleClient a1,HasSimpleClient a2) => HasSimpleClient (a1 :<|> a2) where
type SimpleClient (a1 :<|> a2) = SimpleClient a1 :<|> SimpleClient a2
simpleClientWithRoute _ req =
simpleClientWithRoute (Proxy :: Proxy a1) req :<|>
simpleClientWithRoute (Proxy :: Proxy a2) req
Listing 5.4: Implementierung einer Client-Interpretation (Instanz für :<|>)
Instanzen für die restlichen DSL-Konstrukte werden durch das rekursive „Inlinen“ der
entsprechenden syntaktischen Kategorien in die api-Kategorie realisiert. Bspw. wird die
Ableitung item :> api durch die Ableitungen symbol :> api, Header symbol t :>
api, ReqBody ctypes t :> api usw. ersetzt (analog für method). Auf diese Weise entsteht eine äquivalente Grammatik, mit der es nicht mehr notwendig ist, für jede syntaktische Kategorie eine entsprechende Typklasse zu definieren (z. B. HasSimpleClientItem,
HasSimpleClientMethod). Die HasSimpleClient-Instanz für Pfad-Komponenten des
Kinds Symbol innerhalb von Constraint-Sequenzen wird demnach wie folgt definiert:
1
2
3
4
5
instance (KnownSymbol s,HasSimpleClient a) => HasSimpleClient (s :> a) where
type SimpleClient (s :> a) = SimpleClient a
simpleClientWithRoute _ req =
simpleClientWithRoute (Proxy :: Proxy a) $
req {path = path req ++ "/" ++ symbolVal (Proxy :: Proxy s)}
Listing 5.5: Implementierung einer Client-Interpretation (Instanz für symbols)
Mittels KnownSymbol können wir sicher sein, dass es sich bei s um einen Typ-EbenenString handelt. Ähnlich wie bei :<|> führen wir hier beide Definitionen auf den Rest
der Constraint-Sequenz zurück, da die Pfad-Komponente für den Typ der Client-Funktion
des entsprechenden Endpunkts unerheblich ist. Für die konkrete Anfrage spielt sie aber
natürlich eine Rolle, weshalb wir den korrespondierenden String-Wert mittels symbolVal
extrahieren und hinten an den aktuellen Pfad anfügen müssen.
Als nächstes betrachten wir die Instanz für das ReqBody-Konstrukt. ReqBody ctypes t
spezifiert, dass Anfragen an den zugehörigen Endpunkt Nutzdaten eines Content Types
aus der Liste ctypes im Nachrichtenrumpf enthalten müssen, wobei diese Nutzdaten in
Haskell durch einen Wert des Typs t dargestellt werden. Client-Funktionen für Endpunkte,
die das ReqBody-Konstrukt enthalten, sollten also sinnigerweise einen Parameter des Typs
t erwarten. Es ergibt sich folgende HasSimpleClient-Instanz:
1
2
3
4
5
6
7
instance (MimeRender ctype t,HasSimpleClient a) =>
HasSimpleClient (ReqBody (ctype ’: ctypes) t :> a) where
type SimpleClient (ReqBody (ctype ’: ctypes) t :> a) = t -> SimpleClient a
simpleClientWithRoute _ req b =
simpleClientWithRoute (Proxy :: Proxy a) $
req {body = Just (mimeRender ctp b,contentType ctp)}
where ctp = Proxy :: Proxy ctype
Listing 5.6: Implementierung einer Client-Interpretation (Instanz für ReqBody)
5 Umsetzung der Konzepte
16
In Zeile 3 fügen wir dem Client-Typ den Parameter t hinzu. Die konkrete Implementierung
des Clients erhält entsprechend ein weiteres Argument b vom Typ t (Zeile 4). In Zeile 6 aktualisieren wir das Req-Objekt bzgl. der Informationen über die zu versendenen Nutzdaten
(Inhalt und Content Type, wobei wir bei mehreren akzeptierten Content Types einfach den
ersten aus der Liste wählen). Zunächst serialisieren wir in der ersten Komponente des Paars
die Nutzdaten des Typs t bzgl. des Content Types ct mittels der Methode mimeRender:
mimeRender :: MimeRender ctype t => Proxy ctype -> t -> ByteString
mimeRender ist die einzige Methode der von Servant exportierten Typklasse MimeRender
(siehe Klassen-Constraint in Zeile 1), über deren Instanziierung man jeweils für einen bestimmten Content Type konkrete Serialisierungsfunktionen zur Umwandlung von Werten in
ByteStrings registrieren kann. Für die vordefinierten Content Types stellt Servant bereits
einige Instanzen bereit (z. B. MimeRender PlainText Text). In der zweiten Komponente
des body-Paars wandeln wir schließlich den durch den Typ ct gegebenen Content Type in
einen Wert des Typs MediaType um. contentType ist wiederum die einzige Methode der
von Servant exportierten Typklasse Accept, welche aber von MimeRender impliziert wird.
Die Typklassen MimeRender und Accept sind Bestandteil eines vordefinierten Gerüsts für
die Definition anwendungsspezifischer Content-Type-Konstrukte und deren Interpretation.
Dieses soll in dieser Arbeit allerdings nicht weiter betrachtet werden. Für eine tiefgehendere
Beschreibung dieser Mechanismen sei daher auf [MHAL15] verwiesen.
Zu guter Letzt implementieren wir beispielhaft für die method-Konstrukte eine Instanz für
das Post-Kontrukt (die anderen Methoden interpretieren wir nach dem selben Schema):
1
2
3
4
5
6
7
instance (MimeUnrender ctype rtype) =>
HasSimpleClient (Post (ctype ’: ctypes) rtype) where
type SimpleClient (Post (ctype ’: ctypes) rtype) = EitherT ReqError IO rtype
simpleClientWithRoute _ req = do
rspBody <- performRequest req {method = POST,accept = Just $ contentType ctp}
either (left . ReqError) right $ mimeUnrender ctp rspBody
where ctp = Proxy :: Proxy ctype
Listing 5.7: Implementierung einer Client-Interpretation (Instanz für Post)
Als (konzeptionell garantiert) letztes Element der Constraint-Sequenz eines jeden Endpunkts bilden die method-Konstrukte den Rekursionsschluss. Aus diesem Grund wird an
dieser Stelle das zuvor sukzessiv aufgebaute Req-Objekt mit der Methode und dem Content Type für den Accept-Header (wieder der erste aus der Liste) vervollständigt und der
entsprechende HTTP-Request mittels performRequest an den Host gesendet (Zeile 5). Als
Rückgabe erhalten wir den Inhalt des Response-Bodys als ByteString1 , welchen wir in
Zeile 6 mittels mimeUnrender (analog zu mimeRender die einzige Methode der Typklasse
MimeUnrender) deserialisieren:
mimeUnrender :: MimeUnrender ctype t => Proxy ctype -> ByteString -> Either String t
mimeUnrender liefert uns entweder einen Wert des spezifizierten Rückgabetyps rtype oder
eine (Parser-)Fehlernachricht in Form eines Either-Werts, welchen wir in die EitherT1 Der
Einfachheit halber verzichten wir bei diesem Beispiel auf die eigentlich notwendige Fehlerbehandlung
und gehen davon aus, dass performRequest im Falle eines Fehlers das Programm beendet.
5 Umsetzung der Konzepte
17
Monade liften. Die Fehlernachricht transformieren wir dabei zu einem ReqError. Dadurch
erhalten wir für unseren Clienten letztlich den eingangs festgelegten Rückgabetyp EitherT
ReqError IO rtype, was sich auch in der type-Definition in Zeile 3 wiederspiegelt.
Über die Funktion simpleClient, die simpleClientWithRoute mit einem StandardReq-Objekt initialisiert, können wir nun systematisch Client-Funktionen aus beliebigen
API-Beschreibungen ableiten, sofern diese aus den Konstrukten bestehen, für die wir
HasSimpleClient-Instanzen implementiert haben:
simpleClient :: (HasSimpleClient api) => Proxy api -> SimpleClient api
simpleClient proxy = simpleClientWithRoute proxy $ Req "" GET Nothing Nothing
Für den einzigen Endpunkt der minimalen API aus Kapitel 4 erhalten wir bspw. eine
(parameterlose) Client-Funktion vom Typ EitherT ReqError IO Text, die wir im GHCI
mittels runEitherT ausführen können:
*Main> let helloWorldClient = simpleClient (Proxy :: Proxy HelloWorldAPI)
*Main> runEitherT helloWorldClient
Right "\"Hello world!\""
Für APIs mit mehreren Endpunkten berechnet simpleClient mehrere, durch den Konstruktor :<|> verknüpfte Client-Funktionen, welche wir mittels Pattern Matching extrahieren
können. Um dies zu demonstrieren, erweitern wir die API aus Kapitel 4 um die Beschreibung
eines Endpunkts /echo, der den Inhalt eines POST-Requests an den Client zurücksendet:
type TestAPI = "hello" :> "world" :> Get ’[PlainText] Text
:<|> "echo" :> ReqBody ’[PlainText] Text :> Post ’[PlainText] Text
Mit simpleClient erhalten wir für diese API neben der nullstelligen helloWorldClientFunktion nun auch eine Funktion mit dem Typ Text -> EitherT ReqError IO Text für
den zweiten Endpunkt:
*Main> let helloWorldClient :<|> echoClient = simpleClient (Proxy :: Proxy TestAPI)
*Main> runEitherT $ echoClient $ pack "Hello!"
Right "\"Hello!\""
Die entsprechende Antwort erhalten wir hier natürlich nur, wenn der spezifizierte Webserver unter localhost:8000 erreichbar ist. Einen solchen Webserver können wir ähnlich
wie in Listing 4.1 über die Server-Interpretation implementieren und starten. Der vollständige Code der Client-Interpretation mit Verwendungsbeispiel und passender ServerImplementierung ist in Anhang A zu finden.
Wie wir in diesem Abschnitt gesehen haben, lassen sich also über Typklassen typsichere, Spezifikations-konforme Programm-Komponenten aus zunächst abstrakten APIBeschreibungen ableiten. Typischerweise wird dabei über eine Typ-Funktion der konkrete
Typ der Programm-Komponente berechnet, indem der leere API-Typ auf einen nicht-leeren
(Funktions-)Typ abgebildet wird. Dieser setzt sich dann aus den Typen zusammen, die durch
die API-Beschreibung spezifiziert wurden. Die konkrete Implementierung der ProgrammKomponente ergibt sich wiederum durch Klassenmethoden, die induktiv über den Aufbau
der API-Beschreibung definiert werden. Dabei ist anzumerken, dass die Konstruktionsvorschriften für API-Beschreibungen ohne Interpretationen zunächst nur konzeptionellen
Charakter haben. So gewährleistet die in Abschnitt 5.1.2 beschriebene Implementierung
5 Umsetzung der Konzepte
18
der DSL nicht, dass mit den Konstrukten nur Typen gebaut werden können, die auch tatsächlich aus der DSL ableitbar sind. Bspw. sind auch Post ’[] Int :> "hello" und ()
:> Get ’[PlainText] Int valide Haskell-Typen. Erst Interpretationen mittels Typklassen
garantieren diesen Grad der Typsicherheit, da durch die rekursiv definierten Instanzen
exakt festgelegt werden kann, welche Konstrukte auf welche Weise kombiniert werden
dürfen. Für die obigen, ungültigen API-Beschreibungen ist mit unserer Interpretation z. B.
kein Client ableitbar, da keine passenden HasSimpleClient-Instanzen existieren. Zusätzlich ermöglichen Typklassen die zweite Art der in Punkt (4) geforderten Erweiterbarkeit.
So sind (exportierte) Typklassen standardmäßig offen, d.h. für bestehende Interpretationen aus potentiell anderen Modulen können neue Instanzen (z. B. für selbst definierte
DSL-Konstrukte) hinzugefügt werden, ohne bestehenden Code neu übersetzen zu müssen.
Gleichzeitig können für bereits definierte DSL-Konstrukte jederzeit neue Interpretationen
in Form von Typklassen definiert werden – so wie wir es in diesem Abschnitt getan haben.
Die von Servant verwendete Kombination aus der Repräsentation von Daten durch Typen
und Typklassen über diesen ist somit eine mögliche Lösung für das Expression-Problem.
5.2.2. Verfügbare Interpretationen
Die im letzten Abschnitt implementierte Interpretation ist eine vereinfachte Variante der
Client-Interpretation aus dem Paket servant-client2 . Analog zu unserer Implementierung wird die Schnittstelle hier im Wesentlichen durch die Typklasse HasClient, den Typ
Client api und die Funktion client gebildet. Im Gegensatz zu simpleClient erwartet
client als weiteren Parameter die Basis-URL, welche wir vereinfachend innerhalb von
performRequest festgelegt hatten.
In Kapitel 4 haben wir zudem bereits das Paket servant-server kennengelernt, mit dem
sich API-konforme Webserver implementieren lassen. Analog zu servant-client besteht
die Schnittstelle hier aus der Typklasse HasServer, dem Typ Server api und der Funktion
serve (siehe Zeile 12 in Listing 4.1):
serve :: HasServer api => Proxy api -> Server api -> Application
Im Falle der Server-Interpretation können die Handler-Funktionen für die einzelnen Endpunkte einer API natürlich nicht systematisch abgeleitet werden. Stattdessen wird über die
Typklasse HasServer ein Server-Gerüst bereitgestellt, welches durch einen API-konformen
Handler des Typs Server api zu einer Network.Wai.Application vervollständigt wird.
Analog zur Client-Interpretation werden dabei die Handler verschiedener Endpunkte einer
API auf Wert-Ebene mit dem Datenkonstruktor :<|> verknüpft. D. h. für eine API type
api = endp1 :<|> endp2 entspricht der Typ Server api dem Typ Server endp1 :<|>
Server endp2. Auf die Server-Interpretation werden wir im Zuge eines komplexeren
Verwendungsbeispiels von Servant noch einmal im nächsten Kapitel zurückkommen.
Neben servant-client und servant-server gibt es unter anderem Interpretationen zur
systematischen Ableitung von Dokumentation aus API-Beschreibungen (servant-docs3 ),
2 http://hackage.haskell.org/package/servant-client
3 http://hackage.haskell.org/package/servant-docs
5 Umsetzung der Konzepte
19
zur Ableitung von Client-Funktionen in JavaScript (servant-jquery4 ) oder Ruby (lackey5 )
und zur Erzeugung typsicherer HTML-Links (Bestandteil des Kern-Pakets servant6 ). Auch
letztere Interpretation werden wir im nächsten Kapitel näher kennenlernen.
4 http://hackage.haskell.org/package/servant-jquery
5 http://hackage.haskell.org/package/lackey
6 http://hackage.haskell.org/package/servant-0.4.4.5/docs/Servant-Utils-Links.html
6 Verwendungsbeispiel
20
6. Verwendungsbeispiel
In diesem Kapitel soll noch einmal anhand eines komplexeren Beispiels die Verwendung von
Servant demonstriert werden. Wie schon im einführenden Beispiel in Kapitel 4 betrachten
wir dabei wieder den wohl wichtigsten Use Case von Servant – die Implementierung
eines zur einer API-Beschreibung konformen Webservers. Im Gegensatz zum einführenden
Beispiel soll diesmal allerdings ein zustandsbehafteter Server mit mehreren Endpunkten
und einer HTML-Schnittstelle realisiert werden, d. h. der Zustand des Servers soll über
einen Browser abgefragt und verändert werden können.
Konkret wollen wir einen Webserver implementieren, über den wir Zugriff auf eine Datenbank für Bücher und Autoren haben. Es soll möglich sein, sich eine Listenansicht aller
Bücher in der Datenbank generieren zu lassen und neue Bücher bzw. Autoren hinzuzufügen.
Über die ID eines Buches oder eines Autors soll sich zudem eine Einzelansicht abrufen
lassen, die alle Informationen zu dem entsprechenden Datum zusammenfasst. Bevor wir
nun zur Beschreibung der konkreten Schnittstelle unseres Webservers durch die API-DSL
kommen, befassen wir uns zunächst kurz mit der Realisierung der Datenbank, auf die unser
Webserver zugreifen soll, sowie den später benötigten Datentypen.
Realisierung der Datenbank und benötigte Datentypen
Typischerweise beziehen Webserver ihre Daten aus Datenbanksystemen, die die persistente
Datenhaltung des serverseitigen Zustands gewährleisten. Der Einfachheit halber verzichten
wir in diesem Beispiel auf eine sitzungsübergreifende Persistierung und simulieren die
zugrunde liegende Datenbank durch eine transaktionale Variable (TVar):
type Bookstore = TVar ([Book],[Author])
Der Typ Bookstore repräsentiert hier also eine (relationale) Datenbank mit den zwei
Tabellen Book und Author. Die Datentypen Book und Author repräsentieren wiederum
Bücher und Autoren in der Datenbank. Bücher bestehen dabei aus einer eindeutigen ID des
Typs BookId, einem Titel, einer Beschreibung, dem Jahr der Veröffentlichung und der ID
des Autors. Autoren haben analog eine ID des Typs AuthorId und einen Namen:
newtype BookId =
data Book = Book
{ bookId
, description
, bookAuthorId
}
BookId { getBid :: Int } deriving (Eq)
:: BookId, title :: String
:: String, year :: Int
:: AuthorId
newtype AuthorId = AuthorId { getAid :: Int } deriving (Eq)
data Author = Author { authorId :: AuthorId, name :: String }
21
6 Verwendungsbeispiel
Da wir Bücher später immer mit ihrem zugehörigen Autor bzw. Autoren immer mit den von
ihnen verfassten Büchern betrachten wollen, definieren wir zusätzlich zwei entsprechende
Produkttypen BookData und AuthorData:
type BookData
= (Book,Author)
type AuthorData = (Author,[Book])
Zusätzlich nehmen wir an, dass uns die folgenden Funktionen zur Verfügung stehen:
getAllBookData
insertBookData
lookupBookData
lookupAuthorData
::
::
::
::
Bookstore
Bookstore
Bookstore
Bookstore
->
->
->
->
IO [BookData]
BookData -> IO ()
BookId
-> IO (Maybe BookData)
AuthorId -> IO (Maybe AuthorData)
getAllBookData gibt alle Bücher eines gegebenen Bookstores mit den zugehörigen
Autoren zurück, insertBookData fügt einem Bookstore einen Book- bzw. Author-Eintrag
hinzu, sofern dieser nicht schon in der „Datenbank“ enthalten ist und lookupBookData
sucht im Bookstore das Buch mit der gegebenen BookId und gibt dieses ggf. zusammen
mit dem zugehörigen Autor zurück (analog für lookupAuthor).
Implementierung der API mit Servant
Basierend auf den soeben definierten Datentypen lässt sich die eingangs skizzierte Schnittstelle des Webservers mit Servant bspw. wie folgt beschreiben:
1
2
3
4
5
6
7
type BookstoreAPI
= GetBooksEndp :<|> PostBooksEndp :<|> GetBookEndp :<|> GetAuthorEndp
type GetBooksEndp = "books" :> Get ’[HTML] [BookData]
type PostBooksEndp = "books" :> ReqBody ’[FormUrlEncoded] BookData
:> Post ’[HTML] [BookData]
type GetBookEndp
= "books"
:> Capture "id" BookId
:> Get ’[HTML] BookData
type GetAuthorEndp = "authors" :> Capture "id" AuthorId :> Get ’[HTML] AuthorData
Listing 6.1: Implementierung eines Webservers (Beschreibung der API mit Servant)
Wir identifizieren insgesamt vier Endpunkte. Mittels GET-Request an den Endpunkt /books
soll vom Server die Liste aller BookData-Datensätze angefordert werden können (Zeile
3). Über einen POST-Request an /books soll hingegen ein BookData-Objekt an den Server
übermittelt und in die Datenbank eingefügt werden (Zeile 4). Als Antwort soll der Server
daraufhin die aktualisierte Liste der BookData-Objekte zurückschicken (Zeile 5). Die
beiden Endpunkte /books/:id (Zeile 6) und authors/:id (Zeile 7) sollen schließlich
das BookData- bzw. AuthorData-Objekt mit der jeweiligen ID zurückliefern, falls dieses
vorhanden ist. In Haskell repräsentieren wir die entsprechenden IDs dabei sinnigerweise
durch Werte der Typen BookId bzw. AuthorId. In Zeile 1 werden alle Endpunkte mit dem
Typoperator :<|> zu der vollständigen API-Beschreibung des Webservers verknüpft.
Wie eingangs erwähnt, wollen wir für unseren Server eine HTML-Schnittstelle bereitstellen.
Wir wählen deshalb für die von den Endpunkten generierten HTTP-Responses den Content
Type text/html, welcher durch einen (zunächst undefinierten) Typ HTML repräsentiert
6 Verwendungsbeispiel
22
wird. Die Inhalte von HTML-Formularen für POST-Requests werden zudem standardmäßig zu Schlüssel-Wert-Paaren des Content Types application/x-www-form-urlencoded
umgewandelt. Diesen Content Type repräsentieren wir wiederum durch den in Servant
vordefinierten Typ FormUrlEncoded. Auf die Herkunft des Datentyps HTML und die Interpretation der beiden Content-Type-Konstrukte kommen wir später zurück.
Implementierung der Server-Logik
Um die Logik unseres Webservers zu implementieren, verwenden wir erneut die ServerInterpretation aus dem Paket servant-server. Wie wir in Kapitel 4 und in Abschnitt
5.2.2 gesehen haben, müssen wir dazu einen Handler des Types Server BookstoreAPI
definieren, welcher dann von der serve-Funktion in eine Application umgewandelt
werden kann. Gemäß der in Abschnitt 5.2.2 erläuterten Semantik des Typs Server api
für eine API api mit mehreren Endpunkten müssen wir also für die einzelnen Endpunkte
entsprechende Handler definieren, die wir anschließed mit dem Datenkonstruktor :<|> zu
einem Server BookstoreAPI verknüpfen können:
1
2
3
4
bookstoreHandler :: Bookstore -> Server BookstoreAPI
bookstoreHandler bstore =
(getBooksHandler bstore) :<|> (postBooksHandler bstore) :<|>
(getBookHandler bstore) :<|> (getAuthorHandler bstore)
Listing 6.2: Implementierung eines Webservers (Server-Logik, Teil 1)
Als zusätzlichen Parameter für die Handler benötigen wir hier noch den Zustand des Servers
in Form der Bookstore-Datenbank. Die Definition der Endpunkt-Handler können wir nun
im Wesentlichen auf das anfangs vorausgesetzte Interface für Bookstores zurückführen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
getBooksHandler :: Bookstore -> EitherT ServantErr IO [BookData]
getBooksHandler = liftIO . getAllBookData
postBooksHandler :: Bookstore -> BookData -> EitherT ServantErr IO [BookData]
postBooksHandler bstore bdata = do
liftIO $ insertBookData bstore bdata
getBooksHandler bstore -- we can reuse ‘getBooksHandler‘ here!
getBookHandler :: Bookstore -> BookId -> EitherT ServantErr IO BookData
getBookHandler bstore bid = do
res <- liftIO $ lookupBookData bstore bid
maybe (left err404 { errBody = B.pack "book not found" }) right res
getAuthorHandler :: Bookstore -> AuthorId -> EitherT ServantErr IO AuthorData
getAuthorHandler bstore aid = do
res <- liftIO $ lookupAuthorData bstore aid
maybe (left err404 { errBody = B.pack "author not found" }) right res
Listing 6.3: Implementierung eines Webservers (Server-Logik, Teil 2)
6 Verwendungsbeispiel
23
Ähnlich wie der Typ SimpleClient endp unserer Client-Interpretation aus Abschnitt 5.2.1
ist Server endp ein Typ-Synonym, dessen Definition induktiv aus dem Aufbau des Endpunkts endp abgeleitet wird. Analog zu SimpleClient entsteht dabei stets eine (möglicherweise nullstellige) Funktion mit dem Rückgabetyp EitherT ServantErr IO t, wobei
Anzahl und Typen der restlichen Funktionsparameter von den verwendeten Constraints abhängen. Bspw. wird der Typ Server PostBooksEndp auf den Typ BookData -> EitherT
ServantErr IO [BookData] abgebildet, so dass das dekodierte BookData-Objekt aus
dem POST-Request im zugehörigen Request-Handler weiterverarbeitet werden kann. Etwaige Fehler beim Verarbeiten von Requests werden von der Server-Interpretation über Werte
des Typs ServantErr in entsprechende HTTP-Responses umgewandelt. So generieren wir
bspw. in Zeile 12 über die vordefinierte Funktion err404 eine HTTP-Response mit dem
Statuscode 404, falls in der Datenbank kein Buch mit der gegebenen ID existiert (analog in
Zeile 17 für Autoren).
Zur Dekodierung von Request-Bodys verwendet servant-server die schon bekannte Typklasse MimeUnrender, wobei für den Content Type FormUrlEncoded bereits die folgende
Instanz vordefiniert ist:
instance FromFormUrlEncoded a => MimeUnrender FormUrlEncoded a where -- ...
Um also BookData-Werte aus Request-Bodys des Content Types FormUrlEncoded auslesen zu können, müssen wir eine FromFormUrlEncoded-Instanz für BookData definieren, indem wir die Funktion fromFormUrlEncoded des Typs [(Text,Text)] -> Either
String BookData implementieren:
instance FromFormUrlEncoded BookData where
fromFormUrlEncoded pairs = maybe (Left "could not decode book") Right $ do
title <- fmap T.unpack
$ lookup (T.pack "title")
pairs
descr <- fmap T.unpack
$ lookup (T.pack "description") pairs
year
<- fmap (read . T.unpack) $ lookup (T.pack "year")
pairs
author <- fmap T.unpack
$ lookup (T.pack "author")
pairs
let book
= Book (BookId (-1)) title descr year (AuthorId (-1))
bookAuthor = Author (AuthorId (-1)) author
return (book,bookAuthor)
Wir gehen dabei davon aus, dass Titel, Beschreibung, Jahr und der Name des Autors unter
den Schlüsseln title, description, year und author in den Query-String im RequestBody kodiert werden. Für die IDs verwenden wir zunächst Platzhalter, da diese ggf. erst
später beim Anlegen der Datensätze in der Datenbank vergeben werden.
Weiterhin benötigen wir für unsere Schlüssel-Typen BookId und AuthorId Instanzen der
in servant vordefinierten Typklasse FromText:
class FromText a where
fromText :: Text -> Maybe a
Diese werden von servant-server bei der Interpretation des Capture-Konstrukts benötigt,
um bei Requests an die letzten beiden Endpunkte die in der URL kodierten IDs für die
entsprechenden Handler in Werte der Typen BookId und AuthorId umwandeln zu können.
Die Instanzdefinitionen führen wir auf eine vordefinierte Int-Instanz zurück:
instance FromText BookId
where fromText = fmap BookId
. fromText
instance FromText AuthorId where fromText = fmap AuthorId . fromText
6 Verwendungsbeispiel
24
Implementierung der HTML-Schnittstelle
Im Allgemeinen wählt servant-server zur Kodierung des Rückgabewerts eines EndpunktHandlers für die HTTP-Response einen der Content Types, die durch das entsprechende
method-Konstrukt spezifiziert wurden. Dabei wird bevorzugt der zum Accept-Header des
Requests passende Content Type gewählt, sofern dieser in der Liste enthalten ist. Andernfalls
generiert servant-server eine entsprechende HTTP-Fehlernachricht. Enthält der Request
keinen Accept-Header wird stattdessen einfach der erste Content Type aus der Liste gewählt.
Analog zur Dekodierung von Request-Bodys verwendet servant-server für die Kodierung
von Response-Bodys die Typklasse MimeRender (siehe Abschnitt 5.2.1).
In unserem Fall sollen die Ergebnisse der Handler ausschließlich in Form von HTMLDokumenten an den Client zurückgesendet werden, was wir in Haskell durch den ContentType-Datentyp HTML ausgedrückt haben. Dieser wird durch das Kernpaket servant nicht
selbst definiert. Stattdessen gibt es mit den Paketen servant-blaze1 und servant-lucid2
zwei Bindings für die Haskell-HTML-Bibiotheken blaze-html3 und lucid4 , mit denen
wir die Umwandlung von beliebigen Werten in HTML realisieren können. servant-blaze
und servant-lucid exportieren jeweils den leeren Datentyp HTML und entsprechende
MimeRender-Instanzen, deren konkrete Implementierung wiederum auf blaze- bzw. lucidspezifischen Konvertierungsfunktionen basiert. Bspw. ist in servant-lucid die folgende
MimeRender-Instanz vordefiniert:
instance ToHtml a => MimeRender HTML a where -- ...
Hierbei ist ToHtml die von lucid exportierte Typklasse zur Umwandlung beliebiger Werte
in den von der Bibliothek verwendeten Datentyp zur Darstellung von HTML-Dokumenten.
Wir wollen nun servant-lucid bzw. lucid verwenden, um die HTML-Schnittstelle unseres Webservers zu implementieren. Dazu definieren wir für die Rückgabetypen der
verschiedenen Routen unserer API ToHtml-Instanzen:
instance ToHtml [BookData] where -- ... /books
instance ToHtml BookData
where -- ... /books/:id
instance ToHtml AuthorData where -- ... /authors/:id
Listing 6.4 zeigt die Implementierung der ToHtml-Instanz für [BookData]. Diese beschreibt
nun genau den Inhalt des HTML-Dokuments, welches unser Server bei einem GET- oder
POST-Request an die Route books/ zum Client (dem Browser) zurücksendet. Dies ist der
Fall, da die Request-Handler getBooksHandler bzw. postBooksHandler gemäß unserer
API-Spezifikation Werte vom Typ [BookData] zurückliefern. Konkret besteht das HTMLInterface für /books aus einem Formular für das Anlegen neuer BookData-Einträge per
POST-Request (in den Zeilen 5 bis 15) und einer HTML-Tabelle mit Einträgen für jedes
BookData-Objekt aus der Liste bookDataList (Zeile 3).5 Die Erzeugung dieser Tabelle
wird von der Funktion bookDataListToHtmlTable in Zeile 18 übernommen.
1 http://hackage.haskell.org/package/servant-blaze
2 http://hackage.haskell.org/package/servant-lucid
3 http://hackage.haskell.org/package/blaze-html
4 http://hackage.haskell.org/package/lucid
5 Die
Funktionen b_, form_, action_, usw. sind Teil der lucid-DSL zur Beschreibung von HTML-Dokumenten,
welche an dieser Stelle nicht weiter erläutert werden soll. Dazu sei auf die lucid-Dokumentation verwiesen.
6 Verwendungsbeispiel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
25
-- /books
instance ToHtml [BookData] where
toHtml bookDataList = do
b_ (toHtml "Add a book: "); br_ []
form_ [action_ $ T.pack postBooksUrl,method_ $ T.pack "POST"] $ do
toHtml "Title: "; br_ []
input_ [type_ $ T.pack "text",name_ $ T.pack "title"]; br_ []
toHtml "Description: "; br_ []
input_ [type_ $ T.pack "text",name_ $ T.pack "description"]; br_ []
toHtml "Year: "; br_ []
input_ [type_ $ T.pack "text",name_ $ T.pack "year"]; br_ []
toHtml "Author: "; br_ []
input_ [type_ $ T.pack "text",name_ $ T.pack "author"]; br_ [];
br_ []
input_ [type_ $ T.pack "submit",value_ $ T.pack "Add book"]
hr_ []
b_ (toHtml "All books: "); br_ []
bookDataListToHtmlTable bookDataList
where
postBooksUrl = "/" ++ (show $ safeLink bookstoreAPI postBooksEndp)
Listing 6.4: Implementierung eines Webservers (HTML-Schnittstelle für /books)
Das Formular dient als Schnittstelle zum zweiten Endpunkt unserer Server-API (repräsentiert durch den Typ PostBooksEndp, siehe Listing 6.1). Dem action_-Attribut des
form_-Elements in Zeile 5 könnten wir zu diesem Zweck als Ziel für den Request einfach
die Zeichenkette "/books" in Form eines Text-Wertes übergeben (T.pack "/books").
Stattdessen verwenden wir hier die durch Servant standardmäßig bereitgestellte Interpretation von API-Endpunkten als typsichere HTML-Links (siehe Abschnitt 5.2.2). In Zeile 20
erzeugen wir dazu mit der Funktion safeLink aus der API-Beschreibung und der entsprechenden Endpunkt-Beschreibung den relativen Pfad des Endpunkts auf unserem Server.
Wie üblich übergeben wir die Beschreibungen dabei in Form von Proxy-Objekten:
bookstoreAPI = Proxy :: Proxy BookstoreAPI
postBooksEndp = Proxy :: Proxy PostBooksEndp
Als Ergebnis liefert uns der safeLink-Aufruf den relativen Pfad books in Form eines Wertes
vom Typ URI, welchen wir mit show in die entsprechende String-Darstellung umwandeln. Letztendlich übergeben wir action_ somit tatsächlich einfach nur die Zeichenkette
"/books" als Text-Wert, allerdings würde uns der Compiler nun einen Typfehler melden,
falls wir PostBooksEndp aus der API entfernen oder auf eine Weise verändern, die sich auf
die Signatur von safeLink auswirkt (Captures in der Endpunkt-Beschreibung führen bspw.
zu einem weiteren Argument von safeLink, da der Rückgabetyp wie bei Interpretationen
üblich durch die Anwendung einer Typ-Funktion auf die Endpunkt-Beschreibung bestimmt
wird).
Die Eingabefelder des Formulars wählen wir sinnigerweise passend zu unserer bereits
definierten FromFormUrlEncoded-Instanz für BookData-Werte. Insbesondere müssen wir
dabei darauf achten, dass die Namen der Input-Felder mit den erwarteten Schlüsseln title,
description, year und author übereinstimmen. Auf die konkrete Implementierung von
bookDataListToHtmlTable soll an dieser Stelle verzichtet werden. Diese ist zusammen
6 Verwendungsbeispiel
26
mit den ToHtml-Instanzen für BookData und AuthorData im vollständigen Code des
Webservers in Anhang B zu finden.
Testen des Webservers
Um unseren Webserver nun zu testen, definieren wir eine Datenbank mit Beispielwerten
und eine geeignete main-Funktion, mit der der Server wie schon in Listing 4.1 mittels
serve und run gestartet werden kann:
exampleBookstore
exampleBookstore
where
books
= -authors = --
:: IO Bookstore
= newTVarIO (books,authors)
...
...
main :: IO ()
main = do
bstore <- exampleBookstore
run 8000 $ serve (Proxy :: Proxy BookstoreAPI) (bookstoreHandler bstore)
Die Datenbank müssen wir dabei an den Server-Handler bookstoreHandler übergeben.
Nach dem Starten des Webservers (z. B. über den GHCI) kann dieser über einen Browser
verwendet werden. Abbildung 6.1 zeigt (von links nach rechts) die HTML-Schnittstelle
aus Listing 6.4 (Route /books), die Einzelansicht für Bücher (Route /books/1) und die
Einzelansicht für Autoren mit der Liste ihrer Bücher (Route /authors/0):
Abbildung 6.1.: Implementierung eines Webservers (HTML-Schnittstelle im Browser)
Das Hinzufügen eines neuen Buches über das Formular führt dabei wie erwartet zum
Aktualisieren der Buchliste unter /books, da der Server das aktualisierte HTML-Dokument
als Antwort an den Browser zurücksendet.
7 Zusammenfassung und Diskussion
27
7. Zusammenfassung und
Diskussion
In dieser Arbeit wurde mit Servant ein erweiterbares Haskell-Framework vorgestellt, welches den Kern einer DSL zur Beschreibung von Web-APIs bereitstellt. Mittels Interpretationen können aus solchen abstrakten API-Beschreibungen wiederum konkrete, API-konforme
Software-Komponenten oder -Gerüste abgeleitet werden (z. B. Client-Funktionen, ServerInfrastruktur oder Funktionen zur Generierung der API-Dokumentation). Als wichtigste
Eigenschaften von Servant sind vor allem die zweidimensionale Erweiterbarkeit (um DSLKonstrukte und Interpretationen) und die Typsicherheit der abgeleiteten Komponenten
zu nennen, wobei der Schlüssel zur Erlangung dieser Eigenschaften offensichtlich in der
besonderen Nutzung der Möglichkeiten von Haskells Typsystem liegt. Die Implementierung
der DSL in der Typ-Ebene und das durch diverse Beispiele skizzierte Konstruktionsschema für Interpretationen als Typklassen stellt nicht nur eine mögliche Lösung für das
Expression-Problem dar (und ermöglicht somit die zweidimensionale Erweiterbarkeit),
sondern gewährleistet sozusagen automatisch die gewünschte Typsicherheit. Zudem wird
durch diese Technik eine explizite Trennung von Schnittstelle und Implementierung der
Schnittstelle erreicht, anstatt dass die API nur implizit durch ihre Implementierung gegeben
ist. Bspw. kann mit Servant durch das Typsystem statisch sichergestellt werden, dass mehrere Server-Implementierungen konform zur selben API-Beschreibung sind, was insbesondere
auch dadurch ermöglicht wird, dass Typen in Haskell Bürger erster Klasse sind.
Letztlich ist auch bemerkenswert, dass Servant ein Framework mit einem eher kleinen
Kern ist, dessen Implementierung durchaus überschaubar und somit leichter nachzuvollziehen ist. Dies ist auch wichtig, da für die Nutzung aller Eigenschaften von Servant –
insbesondere für die Definition neuer DSL-Konstrukte und Interpretationen – zweifellos
Kenntnisse über die Implementierung des Frameworks vonnöten sind. Dabei mögen die
verschiedenen verwendeten Erweiterungen von Haskells Typsystem selbst für fortgeschrittene Haskell-Programmierer zunächst neu und ungewohnt sein, jedoch erschließt sich ihr
Nutzen und ihre Verwendung ja unmittelbar durch Servant selbst. Servants kleiner Kern ist
natürlich auch eine direkte Konsequenz aus der Konzeption als erweiterbares Framework.
Anwendungsspezifische Funktionen (DSL-Konstrukte, Interpretationen), die durch die KernBibliothek und andere existente Bibliotheken nicht zur Verfügung gestellt werden, können
mit den von Servant bereitgestellten Mitteln einfach selbst definiert werden. Zwar stellt
Servant selbst schon ausreichend Infrastruktur zur Verfügung, um vollständige Webservices
bzw. Web-Anwendungen zu implementieren, es kann folglich aber auch als Basis für die
Konstruktion individueller, anwendungsspezifischer Web-Frameworks genutzt werden. Die
Erweiterbarkeit ist somit ein wesentlicher Grund für die (scheinbare) Komplexität von
Servant, stellt gleichzeitig aber auch dessen größte Stärke dar.
28
Anhang
A. Vollständiges Beispiel zur
Implementierung einer
Client-Interpretation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{-#
{-#
{-#
{-#
{-#
{-#
LANGUAGE
LANGUAGE
LANGUAGE
LANGUAGE
LANGUAGE
LANGUAGE
import
import
import
import
import
import
import
import
import
import
import
DataKinds #-}
FlexibleInstances #-}
MultiParamTypeClasses #-}
ScopedTypeVariables #-}
TypeFamilies #-}
TypeOperators #-}
Control.Monad.IO.Class
(liftIO)
Control.Monad.Trans.Either
(EitherT,left,right,runEitherT)
Data.ByteString.Lazy.Char8 as B (ByteString,pack,unpack)
Data.Maybe
(fromJust)
Data.Text
as T (Text,pack,unpack)
GHC.TypeLits
(KnownSymbol,Symbol,symbolVal)
Network.HTTP
Network.HTTP.Media
(MediaType)
Network.URI
(parseURI)
Network.Wai.Handler.Warp
(run)
Servant
-- Request interface ----------------------------------------------------------data Req = Req
{ path
:: String,
method :: RequestMethod
, accept :: Maybe MediaType, body
:: Maybe (ByteString,MediaType)
}
data ReqError = ReqError String deriving (Show)
defReq :: Req
defReq = Req "" GET Nothing Nothing
performRequest
performRequest
let httpReq1
httpReq2
:: Req -> EitherT ReqError IO ByteString
(Req path method mAccept mBody) = do
= mkRequest method uri
= maybe httpReq1
(\t -> insertHeader HdrAccept (show t) httpReq1)
mAccept
httpReq3 = maybe httpReq2
(\(b,t) -> setRequestBody httpReq2 (show t,B.unpack b))
mBody
res <- liftIO $ simpleHTTP httpReq3
Anhang
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
29
case res of
Left _
-> left $ ReqError "Oops! Connection error!"
Right rsp -> right $ B.pack $ show $ rspBody rsp
where
mUri = "http://127.0.0.1:8000" ++ if null path then "/" else path
uri = fromJust $ parseURI mUri
-- Client interpretation ------------------------------------------------------class HasSimpleClient api where
type SimpleClient api :: *
simpleClientWithRoute :: Proxy api -> Req -> SimpleClient api
instance (HasSimpleClient a1,HasSimpleClient a2) => HasSimpleClient (a1 :<|> a2) where
type SimpleClient (a1 :<|> a2) = SimpleClient a1 :<|> SimpleClient a2
simpleClientWithRoute _ req =
simpleClientWithRoute (Proxy :: Proxy a1) req :<|>
simpleClientWithRoute (Proxy :: Proxy a2) req
instance (KnownSymbol s,HasSimpleClient a) => HasSimpleClient (s :> a) where
type SimpleClient (s :> a) = SimpleClient a
simpleClientWithRoute _ req =
simpleClientWithRoute (Proxy :: Proxy a) $
req {path = path req ++ "/" ++ symbolVal (Proxy :: Proxy s)}
instance (MimeRender ctype t,HasSimpleClient a) =>
HasSimpleClient (ReqBody (ctype ’: ctypes) t :> a) where
type SimpleClient (ReqBody (ctype ’: ctypes) t :> a) = t -> SimpleClient a
simpleClientWithRoute _ req b =
simpleClientWithRoute (Proxy :: Proxy a) $
req {body = Just (mimeRender ctp b,contentType ctp)}
where ctp = Proxy :: Proxy ctype
instance (KnownSymbol s,ToText t,HasSimpleClient a) =>
HasSimpleClient (Capture s t :> a) where
type SimpleClient (Capture s t :> a) = t -> SimpleClient a
simpleClientWithRoute _ req v =
simpleClientWithRoute (Proxy :: Proxy a) $
req {path = path req ++ "/" ++ (T.unpack $ toText v)}
instance (MimeUnrender ctype rtype) =>
HasSimpleClient (Get (ctype ’: ctypes) rtype) where
type SimpleClient (Get (ctype ’: ctypes) rtype) = EitherT ReqError IO rtype
simpleClientWithRoute _ req = do
rspBody <- performRequest req {method = GET,accept = Just $ contentType ctp}
either (left . ReqError) right $ mimeUnrender ctp rspBody
where ctp = Proxy :: Proxy ctype
instance (MimeUnrender ctype rtype) =>
HasSimpleClient (Post (ctype ’: ctypes) rtype) where
type SimpleClient (Post (ctype ’: ctypes) rtype) = EitherT ReqError IO rtype
simpleClientWithRoute _ req = do
rspBody <- performRequest req {method = POST,accept = Just $ contentType ctp}
either (left . ReqError) right $ mimeUnrender ctp rspBody
where ctp = Proxy :: Proxy ctype
simpleClient :: (HasSimpleClient api) => Proxy api -> SimpleClient api
Anhang
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
simpleClient proxy = simpleClientWithRoute proxy $ Req "" GET Nothing Nothing
-- Test framework -------------------------------------------------------------instance MimeRender PlainText Int where
mimeRender _ = B.pack . show
instance MimeUnrender PlainText Int where
mimeUnrender _ bstr = Right $ read $ drop 1 $ take (length str - 1) str
where str = B.unpack bstr
type TestAPI = "hello" :> "world" :> Get ’[PlainText] Text
:<|> "echo" :> ReqBody ’[PlainText] Text :> Post ’[PlainText] Text
:<|> "echo" :> Capture "this" Int :> Get ’[PlainText] Int
testAPI :: Proxy TestAPI
testAPI = Proxy
helloWorldClient :<|> echoBodyClient :<|> echoCaptureHandler = simpleClient testAPI
testAPIHandler :: Server TestAPI
testAPIHandler = helloWorldHandler :<|> echoBodyHandler :<|> echoCaptureHandler
where
helloWorldHandler = return $ T.pack "Hello world!"
echoBodyHandler text = return $ T.pack $ "Received: " ++ T.unpack text
echoCaptureHandler = return
main :: IO ()
main = run 8000 $ serve (Proxy :: Proxy TestAPI) testAPIHandler
30
31
Anhang
B. Vollständiges Beispiel zur
Implementierung eines
Webservers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeOperators #-}
import
import
import
import
import
import
import
import
import
import
import
import
import
Control.Concurrent.STM
Control.Monad
Control.Monad.IO.Class
Control.Monad.Trans.Either
Data.ByteString.Lazy.Char8 as B (pack)
Data.List
Data.Text
as T (pack,unpack)
Lucid
Lucid.Base
Network.Wai.Handler.Warp
Servant
Servant.HTML.Lucid
Servant.Server
-- Datatypes and database interface -------------------------------------------type Bookstore = TVar ([Book],[Author])
newtype BookId =
data Book = Book
{ bookId
, title
, description
, year
, bookAuthorId
}
BookId { getBid :: Int } deriving (Eq)
::
::
::
::
::
BookId
-- primary key
String
-- unique in combination with bookAuthorId
String
Int
AuthorId -- foreign key, unique in combination with title
newtype AuthorId = AuthorId { getAid :: Int } deriving (Eq)
data Author = Author
{ authorId :: AuthorId -- primary key
, name
:: String
-- unique
}
instance Show BookId
where show (BookId
bid) = show bid
instance Show AuthorId where show (AuthorId aid) = show aid
instance ToText BookId
where toText (BookId
bid) = toText bid
Anhang
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
32
instance ToText AuthorId where toText (AuthorId aid) = toText aid
instance FromText BookId
where fromText = fmap BookId
. fromText
instance FromText AuthorId where fromText = fmap AuthorId . fromText
type BookData = (Book,Author)
type AuthorData = (Author,[Book])
instance FromFormUrlEncoded BookData where
fromFormUrlEncoded pairs = maybe (Left "could not decode book") Right $ do
title <- fmap T.unpack
$ lookup (T.pack "title") pairs
descr <- fmap T.unpack
$ lookup (T.pack "description") pairs
year
<- fmap (read . T.unpack) $ lookup (T.pack "year") pairs
author <- fmap T.unpack
$ lookup (T.pack "author") pairs
let book
= Book (BookId (-1)) title descr year (AuthorId (-1))
bookAuthor = Author (AuthorId (-1)) author
return (book,bookAuthor)
getAllBookData :: Bookstore -> IO [BookData]
getAllBookData bstore = do
(books,authors) <- liftIO $ atomically $ readTVar bstore
return $ map (\book -> (book,authors !! (getAid $ bookAuthorId book))) books
insertBookData :: Bookstore -> BookData -> IO ()
insertBookData bstore (book,author) = liftIO $ atomically $ do
(books,authors) <- readTVar bstore
let newAuthor
= author { authorId = AuthorId $ length authors }
(authorsNew,aid) = maybe (authors++[newAuthor],authorId newAuthor)
(\oldAuthor -> (authors,authorId oldAuthor))
(find ((name newAuthor==).name) authors)
newBook
= book { bookId = BookId $ length books, bookAuthorId = aid }
booksOfAuthor
= filter ((aid==).bookAuthorId) books
booksNew
= maybe (books ++ [newBook])
(const books)
(find ((title book==).title) booksOfAuthor)
writeTVar bstore (booksNew,authorsNew)
lookupBookData :: Bookstore -> BookId -> IO (Maybe BookData)
lookupBookData bstore (BookId bid) = do
(books,authors) <- liftIO $ atomically $ readTVar bstore
if (bid >= 0 && length books > bid)
then do let book = books !! bid
return $ Just (book,authors !! (getAid $ bookAuthorId book))
else return Nothing
lookupAuthorData :: Bookstore -> AuthorId -> IO (Maybe AuthorData)
lookupAuthorData bstore (AuthorId aid) = do
(books,authors) <- liftIO $ atomically $ readTVar bstore
if (aid >= 0 && length authors > aid)
then return $ Just (authors !! aid,filter ((aid==).getAid.bookAuthorId) books)
else return Nothing
-- Bookstore API --------------------------------------------------------------type BookstoreAPI = GetBooksEndp :<|> PostBooksEndp :<|> GetBookEndp :<|> GetAuthorEndp
type GetBooksEndp = "books" :> Get ’[HTML] [BookData]
type PostBooksEndp = "books" :> ReqBody ’[FormUrlEncoded] BookData
33
Anhang
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
:> Post ’[HTML] [BookData]
type GetBookEndp
= "books"
:> Capture "id" BookId
:> Get ’[HTML] BookData
type GetAuthorEndp = "authors" :> Capture "id" AuthorId :> Get ’[HTML] AuthorData
bookstoreAPI
getBooksEndp
postBooksEndp
getBookEndp
getAuthorEndp
=
=
=
=
=
Proxy
Proxy
Proxy
Proxy
Proxy
::
::
::
::
::
Proxy
Proxy
Proxy
Proxy
Proxy
BookstoreAPI
GetBooksEndp
PostBooksEndp
GetBookEndp
GetAuthorEndp
-- Server handler -------------------------------------------------------------bookstoreHandler :: Bookstore -> Server BookstoreAPI
bookstoreHandler store =
(getBooksHandler store) :<|>
(postBooksHandler store) :<|>
(getBookHandler
store) :<|>
(getAuthorHandler store)
getBooksHandler :: Bookstore -> EitherT ServantErr IO [BookData]
getBooksHandler = liftIO . getAllBookData
postBooksHandler :: Bookstore -> BookData -> EitherT ServantErr IO [BookData]
postBooksHandler bstore bdata = do
liftIO $ insertBookData bstore bdata
getBooksHandler bstore
getBookHandler :: Bookstore -> BookId -> EitherT ServantErr IO BookData
getBookHandler bstore bid = do
res <- liftIO $ lookupBookData bstore bid
maybe (left err404 { errBody = B.pack "book not found" }) right res
getAuthorHandler :: Bookstore -> AuthorId -> EitherT ServantErr IO AuthorData
getAuthorHandler bstore aid = do
res <- liftIO $ lookupAuthorData bstore aid
maybe (left err404 { errBody = B.pack "author not found" }) right res
-- HTML views ------------------------------------------------------------------- /books
instance ToHtml [BookData] where
toHtml bookDataList = do
b_ (toHtml "Add a book: "); br_ []
form_ [action_ $ T.pack postBooksUrl,method_ $ T.pack "POST"] $ do
toHtml "Title: "; br_ []
input_ [type_ $ T.pack "text",name_ $ T.pack "title"]; br_ []
toHtml "Description: "; br_ []
input_ [type_ $ T.pack "text",name_ $ T.pack "description"]; br_ []
toHtml "Year: "; br_ []
input_ [type_ $ T.pack "text",name_ $ T.pack "year"]; br_ []
toHtml "Author: "; br_ []
input_ [type_ $ T.pack "text",name_ $ T.pack "author"]; br_ [];
br_ []
input_ [type_ $ T.pack "submit",value_ $ T.pack "Add book"]
hr_ []
b_ (toHtml "All books: "); br_ []
bookDataListToHtmlTable bookDataList
Anhang
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
34
where
postBooksUrl = "/" ++ (show $ safeLink bookstoreAPI postBooksEndp)
-- /books/:id
instance ToHtml BookData where
toHtml (book,author) = do
a_ [href_ $ T.pack getBooksUrl] $ toHtmlRaw "← Back"
p_ $ do
b_ (toHtml "Title: "); toHtml (title book); br_ []
b_ (toHtml "Year: "); toHtml (show $ year book); br_ []
b_ (toHtml "Author: "); a_ [href_ $ T.pack getAuthorUrl] $ toHtml (name author); br_ []
br_ []
b_ (toHtml "Description: "); toHtml (description book); br_ []
where
getBooksUrl = "/" ++ (show $ safeLink bookstoreAPI getBooksEndp)
getAuthorUrl = "/" ++ (show $ safeLink bookstoreAPI getAuthorEndp $ authorId author)
-- /authors/:id
instance ToHtml AuthorData where
toHtml (author,books) = do
a_ [href_ $ T.pack getBooksUrl] $ toHtmlRaw "← Back"
p_ $ do
b_ (toHtml "Author: "); toHtml (name author); br_ []
br_ []
b_ (toHtml "Books: ")
bookDataListToHtmlTable $ map (\book -> (book,author)) books
where
getBooksUrl = "/" ++ (show $ safeLink bookstoreAPI getBooksEndp)
bookDataListToHtmlTable :: (Monad m) => [BookData] -> HtmlT m ()
bookDataListToHtmlTable [] = i_ $ toHtml "no books found"
bookDataListToHtmlTable bookDataList =
table_ [makeAttribute (T.pack "border") (T.pack "1")] $ do
tr_ $ do
th_ $ toHtml "ID"
th_ $ toHtml "Title"
th_ $ toHtml "Year"
th_ $ toHtml "Author"
sequence_ $ map bookDataToHtmlTableRow bookDataList
bookDataToHtmlTableRow :: (Monad m) => BookData -> HtmlT m ()
bookDataToHtmlTableRow (book,author) =
tr_ $ do
td_ $ toHtml $ show $ bookId book
td_ $ a_ [href_ $ T.pack getBookUrl] $ toHtml $ title book
td_ $ toHtml $ show $ year book
td_ $ a_ [href_ $ T.pack getAuthorUrl] $ toHtml $ name author
where
getBookUrl
= "/" ++ (show $ safeLink bookstoreAPI getBookEndp $ bookId book)
getAuthorUrl = "/" ++ (show $ safeLink bookstoreAPI getAuthorEndp $ authorId author)
-- Starting the server --------------------------------------------------------main :: IO ()
main = do
bstore <- exampleBookstore
run 8000 $ serve (Proxy :: Proxy BookstoreAPI) (bookstoreHandler bstore)
Anhang
213
214
215
216
217
218
219
220
221
222
223
224
225
226
35
exampleBookstore :: IO Bookstore
exampleBookstore = newTVarIO (books,authors)
where
books = [hamlet,othello,galileo,baal,illuminati]
hamlet
= Book (BookId 0) "Hamlet" "Hamlet, Hamlet, Hamlet!" 1603 (AuthorId 0)
othello
= Book (BookId 1) "Othello" "Oooooooothello." 1622 (AuthorId 0)
galileo
= Book (BookId 2) "Galileo" "Galileo, Galileo?" 1943 (AuthorId 2)
baal
= Book (BookId 3) "Baal" "Baal..." 1922 (AuthorId 2)
illuminati = Book (BookId 4) "Illuminati" "Illuminati! Illuminati!" 2003 (AuthorId 1)
authors = [shakespeare,brown,brecht]
shakespeare = Author (AuthorId 0) "William Shakespeare"
brown
= Author (AuthorId 1) "Dan Brown"
brecht
= Author (AuthorId 2) "Bertolt Brecht"
36
Literaturverzeichnis
Literaturverzeichnis
[EC14]
E KBLAD, Anton ; C LAESSEN, Koen: A Seamless, Client-centric Programming Model for Type Safe Web Applications. In: Proceedings of the 2014
ACM SIGPLAN Symposium on Haskell, ACM, 2014, S. 79–89. – http:
//haste-lang.org/haskell14.pdf
[Has15]
H ASKELL W IKI: Haskell in industry. https://wiki.haskell.org/Haskell_
in_industry, 2015. – Letzter Abruf: 18.12.2015
[LH06]
L ÖH, Andres ; H INZE, Ralf: Open Data Types and Open Functions. In: PPDP
2006, ACM, 2006, S. 133–144
[M+ 10]
M ARLOW, Simon u. a.: Haskell 2010 — Language Report. (2010). – https:
//www.haskell.org/definition/haskell2010.pdf
[MHAL15]
M ESTANOGULLARI, Alp ; H AHN, Sönke ; A RNI, Julian K. ; L ÖH, Andres:
Type-level Web APIs with Servant: An Exercise in Domain-specific Generic Programming. In: WGP 2015, ACM, 2015, S. 1–12. – siehe auch:
http://haskell-servant.github.io/
[Mor14]
M ORGAN, Jonathon:
ming.
(2014). –
Don’t Be Scared Of Functional Program-
http://www.smashingmagazine.com/2014/07/
dont-be-scared-of-functional-programming/
[OSV08]
O DERSKY, Martin ; S POON, Lex ; V ENNERS, Bill: Programming in Scala: A
Comprehensive Step-by-step Guide. Artima Incorporation, 2008
[PJVWW06] P EYTON J ONES, Simon ; V YTINIOTIS, Dimitrios ; W EIRICH, Stephanie ; WASH BURN, Geoffrey: Simple Unification-based Type Inference for GADTs. In: ICFP
2006, ACM, 2006, S. 50–61
[Sha]
S HAW, Jeremy: The Happstack Book: Modern, Type-Safe Web Development in Haskell. . – http://www.happstack.com/docs/crashcourse/
happstack-book.pdf
[SPJCS08]
S CHRIJVERS, Tom ; P EYTON J ONES, Simon ; C HAKRAVARTY, Manuel ; S ULZ MANN , Martin: Type Checking with Open Type Functions. In: ICFP 2008,
ACM, 2008, S. 51–62
[Wad98]
WADLER, Philip: The Expression Problem, 1998. – http://homepages.inf.
ed.ac.uk/wadler/papers/expression/expression.txt
[YWC+ 12]
Y ORGEY, Brent A. ; W EIRICH, Stephanie ; C RETIN, Julien ; P EYTON J ONES,
Simon ; V YTINIOTIS, Dimitrios ; M AGALHÃES, José P.: Giving Haskell a
Promotion. In: TLDI 2012, ACM, 2012, S. 53–66
Herunterladen