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 "&larr; 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 "&larr; 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