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:
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
.
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.
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.
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)
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
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
@Carsten Oh. Non sapevo che ci fosse un documento. Grazie :) – Ana