2015-10-31 4 views
30

Ich bin sehr verwirrt, wie Servant in der Lage ist, die Magie zu erreichen, die es beim Tippen macht. Das Beispiel auf der Web-Site verwirrt mir schon stark:Welche Mechanismen werden verwendet, um die typbasierte API von Servant zu aktivieren?

type MyAPI = "date" :> Get '[JSON] Date 
     :<|> "time" :> Capture "tz" Timezone :> Get '[JSON] Time 

ich das „Datum“ erhalten, „Zeit“, [JSON] und „tz“ ist Typ-Level-Literale. Sie sind Werte, die "geworden" sind. Okay.

Ich bekomme, dass :> und :<|> sind Typ Operatoren. Okay.

Ich verstehe nicht, wie diese Dinge, nachdem sie zu Typen geworden sind, wieder in Werte extrahiert werden können. Was ist der Mechanismus dafür?

Ich bekomme auch nicht, wie der erste Teil dieses Typs kann das Framework eine Funktion der Signatur IO Date erwarten, oder wie der zweite Teil dieses Typs kann das Framework eine Funktion der Signatur Timezone -> IO Time erwarten von mir. Wie geschieht diese Transformation?

Und wie kann das Framework dann eine Funktion aufrufen, für die es den Typ zunächst nicht kannte?

Ich bin mir sicher, dass es hier eine Reihe von GHC-Erweiterungen und einzigartigen Features gibt, die mir nicht vertraut sind, um diese Magie passieren zu lassen.

Kann jemand erklären, welche Funktionen hier beteiligt sind und wie sie zusammenarbeiten?

+2

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

+0

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

+0

@ Carsten Oh. Ich wusste nicht, dass es eine Zeitung gibt. Danke :) – Ana

Antwort

34

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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) 
Verwandte Themen