Mit Blick auf die Servant paper für eine vollständige Erklärung kann die beste Option sein. Nichtsdestoweniger werde ich versuchen, den von Servant übernommenen Ansatz hier zu veranschaulichen, indem ich "TinyServant", eine auf das absolute Minimum reduzierte Version von Servant, implementiere.
Entschuldigung, dass diese Antwort so lang ist. Allerdings ist es immer noch ein bisschen kürzer als das Papier, und der Code hier diskutiert ist "nur" 81 Zeilen, auch als Haskell-Datei here.
Vorbereitungen
, hier zu starten sind die Spracherweiterungen wir brauchen werden:
{-# LANGUAGE DataKinds, PolyKinds, TypeOperators #-}
{-# LANGUAGE TypeFamilies, FlexibleInstances, ScopedTypeVariables #-}
{-# LANGUAGE InstanceSigs #-}
Die ersten drei sind für die Definition des DSL- selbst Typ-Ebene erforderlich. Die DSL verwendet Strings auf Typenebene (DataKinds
) und auch verwendet Artenpolymorphie (PolyKinds
). Die Verwendung der Infix-Operatoren auf Typenebene wie :<|>
und :>
erfordert die Erweiterung TypeOperators
.
Die zweite drei für die Definition der Interpretation benötigt werden (wir etwas erinnert, was ohne die gesamte Web-Teil ein Web-Server der Fall ist, sondern definieren werden). Dazu benötigen wir Funktionen auf Typenebene (TypeFamilies
), einige Typklassenprogrammierung, die (FlexibleInstances
) erfordern, und einige Typ Anmerkungen, um den Typ Checker zu leiten, die ScopedTypeVariables
erfordern.
rein zu Dokumentationszwecken verwenden wir auch InstanceSigs
.
Hier ist unser Modul-Header:
module TinyServant where
import Control.Applicative
import GHC.TypeLits
import Text.Read
import Data.Time
Nach diesen Vorbereitungen, sind wir bereit, in Gang zu bringen.
API-Spezifikationen
Der erste Bestandteil ist die Datentypen zu definieren, die verwendet wird für die API-Spezifikationen sind.
data Get (a :: *)
data a :<|> b = a :<|> b
infixr 8 :<|>
data (a :: k) :> (b :: *)
infixr 9 :>
data Capture (a :: *)
Wir definieren nur vier Konstrukte in unserer vereinfachten Sprache:
A Get a
darstellt und Endpunkt des Typs a
(die Art *
). In Vergleich mit Full Servant, ignorieren wir hier Inhaltstypen. Wir benötigen den Datentyp nur für die API-Spezifikationen. Es gibt jetzt direkt entsprechende Werte und somit gibt es keinen Konstruktor für Get
.
Mit a :<|> b
vertreten wir die Wahl zwischen zwei Routen. Auch hier würden wir einen Konstruktor nicht brauchen, aber es stellt sich heraus, dass wir ein Paar Handler verwenden werden, die Prozedur eines API darstellen :<|>
verwenden. Für verschachtelte Anwendungen von :<|>
erhielten wir verschachtelte Handlerpaare, die mit der Standardnotation in Haskell etwas hässlich aussehen. Daher definieren wir den Konstruktor als äquivalent zu einem Paar.
Mit item :> rest
, wir stellen verschachtelt Routen, auf denen item
die erste Komponente und rest
sind die übrigen Komponenten. In unserem vereinfachten DSL gibt es nur zwei Möglichkeiten für item
: ein Typ-Level-String oder ein Capture
. Da Typ-Ebene Strings sind von Art Symbol
, sondern ein Capture
, wie unten definiert ist von Art *
wir das erste Argument von :>
Art-polymorphen machen, so dass beide Optionen von die Art System Haskell akzeptiert werden.
A Capture a
stellt eine Routenkomponente, die erfasst wird, geparst und dann als ein Parameter des Typs a
an den Handler ausgesetzt. In vollem Zustand hat Capture
einen zusätzlichen String als Parameter , der für die Dokumentationsgenerierung verwendet wird. Wir verzichten auf die Zeichenfolge hier.
Beispiel API
können wir nun eine Version der API-Spezifikation von der Frage aufschreiben, in Data.Time
vorkommenden auf die tatsächlichen Typen angepasst und zu unserem vereinfachten DSL:
type MyAPI = "date" :> Get Day
:<|> "time" :> Capture TimeZone :> Get ZonedTime
Interpretation als Server
Der interessanteste Aspekt ist natürlich, was wir tun können mit der API, und das ist auch meistens, was die Frage ist.
Servant definiert mehrere Interpretationen, aber alle folgen einem ähnlichen Muster . Wir werden hier einen definieren, der von der Interpretation als Webserver inspiriert ist.
In Servant, die serve
Funktion nimmt einen Proxy für den API-Typen und einen Handler des API-Typen mit einem WAI passenden Application
, der ist im wesentlichen eine Funktion von HTTP-Anfragen auf Antworten. Wir werden Auszug aus dem Stegteil hier und
serve :: HasServer layout
=> Proxy layout -> Server layout -> [String] -> IO String
stattdessen definieren.
Die HasServer
-Klasse, die wir unten definieren werden, hat Instanzen für all die verschiedenen Konstrukte der Art-Ebene DSL und damit codiert was es bedeutet, für einen layout
Haskell Typ als API Art von interpretierbar zu sein ein Server.
Die Proxy
stellt eine Verbindung zwischen dem Typ und der Wertebene her. Es ist definiert als
data Proxy a = Proxy
und dessen einziger Zweck es ist, dass in einem Proxy
Konstruktor mit einem explizit angegebenen Typs, indem wir es sehr deutlich für das, was API-Typ machen können wir den Server berechnen möchten.
Das Server
Argument ist der Handler für die API
. Hier ist Server
selbst eine Typfamilie und berechnet aus dem API-Typ den Typ , den die Handler haben müssen. Dies ist ein Kernbestandteil von dem, was bewirkt, dass Servant korrekt arbeitet.
Die Liste der Zeichenfolgen stellt die Anfrage dar, reduziert auf eine Liste von URL-Komponenten. Als Ergebnis geben wir immer eine String
Antwort, Antwort und wir erlauben die Verwendung von IO
. Full Servant verwendet etwas mehr komplizierte Typen hier, aber die Idee ist die gleiche.
Die Server
Art Familie
Wir definieren Server
als eine Art Familie an erster Stelle. (In Servant ist die tatsächlich verwendete Typfamilie ServerT
und ist als Teil der HasServer
Klasse definiert.
)
type family Server layout :: *
Der Handler für einen Get a
Endpunkt ist einfach eine Aktion IO
eine a
erzeugen. (Wieder einmal im vollen Servant-Code, haben wir etwas mehr Optionen, wie zum Beispiel einen Fehler zu erzeugen.)
type instance Server (Get a) = IO a
Der Handler für a :<|> b
ein Paar Handler ist, so konnten wir definieren
type instance Server (a :<|> b) = (Server a, Server b) -- preliminary
Aber wie oben angegeben, für verschachtelte Vorkommen von :<|>
dies führt zu verschachtelten Paaren, die mit einem Konstruktor Infix Paar etwas schöner aussehen, so definiert Servant anstelle der äquivalenten
012.351.
type instance Server (a :<|> b) = Server a :<|> Server b
Es bleibt zu erklären, wie jede der Pfadkomponenten behandelt wird.
Literalzeichenfolgen in den Strecken wirken sich nicht auf den Typ des Handler:
type instance Server ((s :: Symbol) :> r) = Server r
Eine Erfassung bedeutet jedoch, dass der Handler ein zusätzliches Argument des Typs erfasst werden erwartet:
type instance Server (Capture a :> r) = a -> Server r
Berechnung der Handler-Typ des Beispiels API
Wenn wir Server MyAPI
erweitern wir OBT
ain
Server MyAPI ~ Server ("date" :> Get Day
:<|> "time" :> Capture TimeZone :> Get ZonedTime)
~ Server ("date" :> Get Day)
:<|> Server ("time" :> Capture TimeZone :> Get ZonedTime)
~ Server (Get Day)
:<|> Server ("time" :> Capture TimeZone :> Get ZonedTime)
~ IO Day
:<|> Server ("time" :> Capture TimeZone :> Get ZonedTime)
~ IO Day
:<|> Server (Capture TimeZone :> Get ZonedTime)
~ IO Day
:<|> TimeZone -> Server (Get ZonedTime)
~ IO Day
:<|> TimeZone -> IO ZonedTime
So wie beabsichtigt, benötigt der Server für unsere API ein Paar Handler, ein, die ein Datum enthält, und eine, die eine Zeitzone angegeben, liefert eine Zeit. Diese können wir jetzt definieren:
handleDate :: IO Day
handleDate = utctDay <$> getCurrentTime
handleTime :: TimeZone -> IO ZonedTime
handleTime tz = utcToZonedTime tz <$> getCurrentTime
handleMyAPI :: Server MyAPI
handleMyAPI = handleDate :<|> handleTime
Die HasServer
Klasse
Wir müssen noch die HasServer
Klasse implementieren, die aussieht, als folgt:
class HasServer layout where
route :: Proxy layout -> Server layout -> [String] -> Maybe (IO String)
Die Aufgabe der Funktion route
ist fast wie serve
. Intern müssen wir eine eingehende Anfrage an den richtigen Router senden. In dem Fall von :<|>
bedeutet dies, dass wir die Wahl zwischen zwei Handlern treffen müssen. Wie treffen wir diese Wahl? Eine einfache Option ist es, route
zu scheitern, indem Sie eine Maybe
zurückgeben. (Wiederum ist der vollständige Servant hier etwas ausgefeilter, und Version 0.5 wird eine viel bessere Routing-Strategie haben.
)
Sobald wir route
definiert haben, können wir leicht serve
von route
in Begriffe definieren:
serve :: HasServer layout
=> Proxy layout -> Server layout -> [String] -> IO String
serve p h xs = case route p h xs of
Nothing -> ioError (userError "404")
Just m -> m
Falls keine der Routen anzeigen lassen, scheitern wir mit einem 404. Ansonsten haben wir das Ergebnis zurück.
Die HasServer
Instanzen
Für einen Get
Endpunkt, definiert wir
type instance Server (Get a) = IO a
so der Handler ein IO-Aktion ist eine a
Herstellung, die wir in eine String
abbiegen. Wir verwenden show
für diesen Zweck. In der tatsächlichen Servant-Implementierung wird diese Konvertierung von den Inhaltstypen Maschinerie gehandhabt, und wird normalerweise Kodierung zu JSON oder HTML umfassen.
instance Show a => HasServer (Get a) where
route :: Proxy (Get a) -> IO a -> [String] -> Maybe (IO String)
route _ handler [] = Just (show <$> handler)
route _ _ _ = Nothing
Da wir nur einen Endpunkt Anpassung erfordert die die Anfrage an dieser Stelle leer zu sein. Wenn nicht, stimmt diese Route nicht mit überein und wir geben Nothing
zurück.
Blick Lassen Sie uns bei Wahl weiter:
instance (HasServer a, HasServer b) => HasServer (a :<|> b) where
route :: Proxy (a :<|> b) -> (Server a :<|> Server b) -> [String] -> Maybe (IO String)
route _ (handlera :<|> handlerb) xs =
route (Proxy :: Proxy a) handlera xs
<|> route (Proxy :: Proxy b) handlerb xs
Hier bekommen wir ein Paar Handler und wir verwenden <|>
für Maybe
beide zu versuchen.
Was passiert bei einer literalen Zeichenfolge?
instance (KnownSymbol s, HasServer r) => HasServer ((s :: Symbol) :> r) where
route :: Proxy (s :> r) -> Server r -> [String] -> Maybe (IO String)
route _ handler (x : xs)
| symbolVal (Proxy :: Proxy s) == x = route (Proxy :: Proxy r) handler xs
route _ _ _ = Nothing
Der Handler für s :> r
ist vom gleichen Typ wie der Handler für r
. Wir verlangen, dass die Anforderung nicht leer ist und die erste Komponente das Gegenstück auf Werteebene der Zeichenfolge auf Textebene entspricht. Wir erhalten die Wertelevel-Zeichenfolge, die dem Zeichenfolgenliteral auf Textebene entspricht, durch unter Anwendung von symbolVal
. Dazu benötigen wir eine KnownSymbol
Einschränkung auf die Zeichenfolge Zeichenfolgenliteratur. Aber alle konkreten Literale in GHC sind automatisch eine Instanz von KnownSymbol
.
Der letzte Fall ist für Aufnahmen:
instance (Read a, HasServer r) => HasServer (Capture a :> r) where
route :: Proxy (Capture a :> r) -> (a -> Server r) -> [String] -> Maybe (IO String)
route _ handler (x : xs) = do
a <- readMaybe x
route (Proxy :: Proxy r) (handler a) xs
route _ _ _ = Nothing
In diesem Fall können wir davon ausgehen, dass unser Handler ist tatsächlich eine Funktion, dass ein a
erwartet. Wir fordern, dass die erste Komponente der Anforderung als als a
analysierbar ist. Hier verwenden wir Read
, während in Servant wir den Inhaltstyp Maschinen erneut verwenden. Wenn das Lesen fehlschlägt, betrachten wir die Anfrage als nicht passend. Andernfalls können wir es dem Handler zuführen und fortfahren.
Alles testen
Jetzt sind wir fertig.
wir, dass alles funktioniert in GHCi bestätigen kann:
GHCi> serve (Proxy :: Proxy MyAPI) handleMyAPI ["time", "CET"]
"2015-11-01 20:25:04.594003 CET"
GHCi> serve (Proxy :: Proxy MyAPI) handleMyAPI ["time", "12"]
*** Exception: user error (404)
GHCi> serve (Proxy :: Proxy MyAPI) handleMyAPI ["date"]
"2015-11-01"
GHCi> serve (Proxy :: Proxy MyAPI) handleMyAPI []
*** Exception: user error (404)
haben Sie bei der [Papier] hatte einen Blick (http://www.andres-loeh.de/Servant/servant-wgp.pdf) ? ... Ich weiß nicht, ob wir eine bessere Erklärung bekommen können als das ... vielleicht liest du es und kommst mit Detailfragen zurück, die du nicht verstehst - die Frage hier ist mindestens so umfassend wie das Papier ist lang;) – Carsten
Die Klasse 'GHC.TypeLits.KnownSymbol' und zugehörige Funktionen werden verwendet, um Zeichenfolgen auf Typenebene (' Symbol') in Zeichenfolgen auf Werteebene zu konvertieren. Der Mechanismus ist für jeden anderen Typ im Wesentlichen gleich: Verwenden Sie eine Typklasse. Um Typen aus anderen Typen zu generieren, können Sie eine Typklasse oder eine Typfamilie verwenden. Die Frage nach "wie" ist ziemlich breit, aber das ist die kurze Version. – user2407038
@ Carsten Oh. Ich wusste nicht, dass es eine Zeitung gibt. Danke :) – Ana