2015-10-31 15 views
30

Sono molto perplesso dal modo in cui Servant è in grado di raggiungere la magia che utilizza digitando. L'esempio sul sito web mi lascia perplesso già notevolmente:Quali sono tutti i meccanismi utilizzati per abilitare l'API basata sul tipo di Servant?

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

ottengo la "data", "tempo", [JSON] e "tz" sono letterali tipo di livello. Sono valori che hanno "diventano" tipi. Va bene.

Ho capito che :> e :<|> sono operatori di tipo. Va bene.

Non capisco come queste cose, dopo essere diventate tipi, possano essere estratte in valori. Qual è il meccanismo per farlo?

Inoltre non capisco come la prima parte di questo tipo possa ottenere il framework per prevedere una funzione della firma IO Date, o come la seconda parte di questo tipo possa ottenere il framework per aspettarsi una funzione della firma Timezone -> IO Time da me. Come avviene questa trasformazione?

E come può quindi il framework chiamare una funzione per la quale inizialmente non conosceva il tipo?

Sono sicuro che ci sono un certo numero di estensioni GHC e caratteristiche uniche in gioco che non mi è familiare per combinare questa magia.

Qualcuno può spiegare quali caratteristiche sono coinvolte qui e come stanno lavorando insieme?

+2

hai avuto uno sguardo alla [carta] (http://www.andres-loeh.de/Servant/servant-wgp.pdf) ? ... Non so se possiamo ottenere una spiegazione migliore di quella ... forse la leggi e ritorna con domande dettagliate che non capisci - la domanda qui è almeno ampia come la carta è lunga;) – Carsten

+0

La classe 'GHC.TypeLits.KnownSymbol' e le funzioni associate sono utilizzate per convertire stringhe a livello di testo (' Symbol') in stringhe a livello di valore. Il meccanismo è essenzialmente lo stesso per qualsiasi altro tipo: usa una classe di tipo. Per generare tipi di altri tipi, è possibile utilizzare una classe di tipi o una famiglia di tipi. La domanda su "come" è abbastanza ampia ma questa è la versione breve. – user2407038

+0

@Carsten Oh. Non sapevo che ci fosse un documento. Grazie :) – Ana

risposta

34

Guardando il numero Servant paper per una spiegazione completa, è possibile che l' sia l'opzione migliore. Tuttavia, cercherò di illustrare l'approccio assunto da Servant qui, implementando "TinyServant", una versione di Servitore ridotta al minimo.

Siamo spiacenti che questa risposta sia così lunga. Tuttavia, è ancora un po 'più breve di rispetto alla carta e il codice discusso qui è "solo" 81 righe, disponibile anche come file Haskell here.

preparativi

Per iniziare, qui ci sono le estensioni del linguaggio avremo bisogno:

{-# LANGUAGE DataKinds, PolyKinds, TypeOperators #-} 
{-# LANGUAGE TypeFamilies, FlexibleInstances, ScopedTypeVariables #-} 
{-# LANGUAGE InstanceSigs #-} 

I primi tre sono necessari per la definizione del tipo di livello DSL stessa. DSL utilizza stringhe di tipo livello (DataKinds) e anche utilizza il polimorfismo di tipo (PolyKinds). L'utilizzo degli operatori di tipo livello come :<|> e :> richiede l'estensione TypeOperators .

I secondi tre sono necessari per la definizione dell'interpretazione (definiremo qualcosa che ricorda quello che fa un server Web, ma senza l'intera web part). Per questo, sono necessarie le funzioni a livello di codice (TypeFamilies), alcune classi di tipi di programmazione che richiedono (FlexibleInstances) e alcune annotazioni di tipo per guidare il correttore tipo che richiede ScopedTypeVariables.

Solo a scopo di documentazione, utilizziamo anche InstanceSigs.

Ecco la nostra intestazione modulo:

module TinyServant where 

import Control.Applicative 
import GHC.TypeLits 
import Text.Read 
import Data.Time 

Dopo questi preliminari, siamo pronti per andare avanti.

specifiche API

Il primo ingrediente è definire i tipi di dati che vengono essere utilizzate a specifiche API.

data Get (a :: *) 

data a :<|> b = a :<|> b 
infixr 8 :<|> 

data (a :: k) :> (b :: *) 
infixr 9 :> 

data Capture (a :: *) 

Definiamo solo quattro costrutti nel nostro linguaggio semplificato:

  1. A Get a rappresenta e endpoint di tipo a (di tipo *). Nel confronto con il server completo, ignoriamo qui i tipi di contenuto. Abbiamo bisogno del il tipo di dati solo per le specifiche API. Ora ci sono direttamente i valori corrispondenti , e quindi non esiste un costruttore per Get.

  2. Con a :<|> b, rappresentiamo la scelta tra due percorsi. Ancora una volta, non avremmo bisogno di un costruttore, ma si scopre che useremo una coppia di gestori per rappresentare il gestore di un'API utilizzando :<|>. Per le applicazioni nidificate di :<|>, otterremmo coppie di gestori nidificati nidificate, che appaiono un po 'brutte usando la notazione standard in Haskell, quindi definiamo il costruttore :<|> equivalente a una coppia.

  3. percorsi Con item :> rest, rappresentiamo nidificata, dove item è il primo componente e rest sono i componenti rimanenti. Nella nostra DSL semplificata, ci sono solo due possibilità per item: una stringa a livello di carattere o Capture. Perché il tipo di livello stringhe sono di tipo Symbol, ma un Capture, di seguito definito è di tipo *, facciamo il primo argomento di :> tipo-polimorfico, in modo che entrambe le opzioni sono accettate da il tipo di sistema Haskell.

  4. Un Capture a rappresenta un componente route catturati, analizzati e poi esposti al gestore come parametro di tipo a. In full Servant, Capture ha una stringa aggiuntiva come parametro utilizzata per la generazione di documentazione. Omettiamo la stringa qui.

Esempio API

Ora possiamo scrivere una versione delle specifiche API dalla questione , adattati ai tipi effettivi che si verificano in Data.Time, e alla nostra DSL semplificata:

type MyAPI = "date" :> Get Day 
     :<|> "time" :> Capture TimeZone :> Get ZonedTime 

Interpretazione come server

L'aspetto più interessante è, naturalmente, quello che possiamo fare con l'API , ed è anche per lo più di cosa si tratta.

Il server definisce diverse interpretazioni, ma tutte seguono uno schema simile a . Ne definiremo uno qui, che è ispirato all'interpretazione di come server web.

In Serva, la funzione serve prende un proxy per il tipo API e un gestore adatto al tipo API per un WAI Application, che è essenzialmente una funzione da richieste HTTP alle risposte. Ci astratta dalla parte web qui, e definiamo

serve :: HasServer layout 
     => Proxy layout -> Server layout -> [String] -> IO String 

invece.

La classe HasServer, che definiremo di seguito, ha istanze per tutti i diversi costrutti del tipo DSL livello e quindi codifica cosa significa per un tipo Haskell layout essere interpretabile come un tipo API un server.

Il Proxy stabilisce una connessione tra il tipo e il livello del valore. E 'definito come

data Proxy a = Proxy 

e il suo unico scopo è che facendo passare in un Proxy costruttore con un tipo specificato esplicitamente, siamo in grado di rendere molto esplicito per quale tipo di API che vogliamo calcolare il server.

L'argomento Server è il gestore per lo API. Qui, Server è di per sé una famiglia di tipi e calcola dal tipo di API il tipo che deve avere il gestore. Questo è uno degli ingredienti principali di ciò che fa funzionare correttamente Servant.

L'elenco di stringhe rappresenta la richiesta, ridotta a un elenco di componenti URL . Di conseguenza, restituiamo sempre una risposta String, e consentiamo l'uso di IO. Full Servant utilizza un po 'più tipi complicati di qui, ma l'idea è la stessa.

Il Server tipo familiare

Definiamo Server come una famiglia tipo di prima. (In Servant, la famiglia di tipi effettiva utilizzata è ServerT e il numero è definito come parte della classe HasServer.)

type family Server layout :: * 

Il gestore di un Get a endpoint è semplicemente un IO azione produrre un a. (Ancora una volta, il codice completo Servo, abbiamo leggermente più opzioni, come ad esempio la produzione di un errore.)

type instance Server (Get a) = IO a 

Il gestore per a :<|> b è una coppia di gestori, così abbiamo potuto definire

type instance Server (a :<|> b) = (Server a, Server b) -- preliminary 

Ma, come indicato sopra, le occorrenze nidificati di :<|> questo porta a coppie nidificate, che sembrano alquanto più con una coppia infisso costruttore, quindi Servo definisce invece l'equivalente

012.351.
type instance Server (a :<|> b) = Server a :<|> Server b 

Rimane da spiegare come viene gestita ciascuna delle componenti del percorso.

stringhe letterali nelle rotte non influenzano il tipo del gestore :

type instance Server ((s :: Symbol) :> r) = Server r 

Un bloccaggio, tuttavia, significa che il gestore si attende un ulteriore argomento del tipo viene catturato:

type instance Server (Capture a :> r) = a -> Server r 

Calcolo del tipo conduttore di esempio API

Se espandiamo Server MyAPI, abbiamo 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 

Così come previsto, il server per la nostra API richiede un paio di gestori, uno che fornisce una data, e uno che, dato un fuso orario, fornisce volta. Possiamo definire questi in questo momento:

handleDate :: IO Day 
handleDate = utctDay <$> getCurrentTime 

handleTime :: TimeZone -> IO ZonedTime 
handleTime tz = utcToZonedTime tz <$> getCurrentTime 

handleMyAPI :: Server MyAPI 
handleMyAPI = handleDate :<|> handleTime 

La classe HasServer

Dobbiamo ancora implementare la classe HasServer, che appare come segue:

class HasServer layout where 
    route :: Proxy layout -> Server layout -> [String] -> Maybe (IO String) 

Il compito della funzione route è quasi come serve. Internamente, , dobbiamo inviare una richiesta in arrivo al router giusto. Nel caso di :<|>, ciò significa che dobbiamo fare una scelta tra due gestori di . Come facciamo questa scelta? Una semplice opzione è quella di consentire route fallire, restituendo un Maybe. (Ancora una volta, full Servant è un po 'più sofisticato qui, e la versione 0.5 avrà una strategia di routing molto più efficace .)

Una volta che abbiamo definito route, possiamo definire facilmente serve in termini di route:

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 

Se nessuno dei percorsi corrisponde, non riusciamo con 404. In caso contrario, ci restituire il risultato.

I HasServer casi

Per un Get endpoint, abbiamo definito

type instance Server (Get a) = IO a 

in modo che il gestore è un'azione IO producendo un a, che dobbiamo di trasformarsi in un String. Usiamo show per questo scopo. In l'effettiva implementazione di Servant, questa conversione viene gestita da dal tipo di computer dei tipi di contenuto e in genere implica la codifica di in JSON o HTML.

instance Show a => HasServer (Get a) where 
    route :: Proxy (Get a) -> IO a -> [String] -> Maybe (IO String) 
    route _ handler [] = Just (show <$> handler) 
    route _ _  _ = Nothing 

Visto che stiamo corrispondenza solo un endpoint, la richiedono la richiesta essere vuoto a questo punto. Se non lo è, questa rotta non corrisponde a e restituiamo Nothing. sguardo

Let a scelta successiva:

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 

Qui, si ottiene una coppia di gestori, e usiamo <|> per Maybe provare entrambi.

Cosa succede per una stringa letterale?

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 

Il gestore per s :> r è dello stesso tipo del gestore per r. Richiediamo che la richiesta sia non vuota e che il primo componente corrisponda a la controparte a livello di valore della stringa a livello di testo. Otteniamo la stringa di livello di valore corrispondente alla stringa di livello del testo letterale di che applica symbolVal. Per questo, abbiamo bisogno di un vincolo KnownSymbol su letterale stringa di tipo. Ma tutti i letterali concreti in GHC sono automaticamente un'istanza di KnownSymbol.

L'ultimo caso è per la cattura:

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 questo caso, si può supporre che il nostro gestore è in realtà una funzione che prevede un a. Richiediamo che il primo componente della richiesta sia analizzabile come a. Qui, usiamo Read, mentre in Servant, usiamo ancora il tipo di contenuto macchinario. Se la lettura fallisce, consideriamo la richiesta non corrispondente. Altrimenti, possiamo dargli da mangiare al gestore e continuare.

Testing tutto

Ora abbiamo finito.

Possiamo confermare che tutto funziona in GHCi:

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)