Ответ 1
Глядя на Рабочую страницу для полного объяснения, можно быть лучшим вариантом. Тем не менее, я попытаюсь проиллюстрировать подход взятый Слуги здесь, путем реализации "TinyServant", версии Слуга сведен к минимуму.
Извините, что этот ответ так длинный. Тем не менее, он все еще немного короче чем в документе, а обсуждаемый здесь код - "всего" 81 строка, доступен также как файл Haskell здесь.
Подготовка
Чтобы начать, вот необходимые нам языковые расширения:
{-# LANGUAGE DataKinds, PolyKinds, TypeOperators #-}
{-# LANGUAGE TypeFamilies, FlexibleInstances, ScopedTypeVariables #-}
{-# LANGUAGE InstanceSigs #-}
Первые три необходимы для определения DSL уровня уровня
сам. DSL использует строки типа (DataKinds
), а также
использует вид полиморфизма (PolyKinds
). Использование инфикса уровня
для операторов, таких как :<|>
и :>
, требуется TypeOperators
расширение.
Вторая тройка необходима для определения интерпретации
(мы определим что-то напоминающее то, что делает веб-сервер, но
без всей веб-части). Для этого нам нужны функции уровня
(TypeFamilies
), некоторые типы программирования классов, которые потребуют
(FlexibleInstances
), а некоторые аннотации типов для указания типа
который требует ScopedTypeVariables
.
Для целей документации мы также используем InstanceSigs
.
Здесь наш заголовок модуля:
module TinyServant where
import Control.Applicative
import GHC.TypeLits
import Text.Read
import Data.Time
После этих предварительных шагов мы готовы идти.
Спецификации API
Первым ингредиентом является определение типов данных, которые используется для спецификаций API.
data Get (a :: *)
data a :<|> b = a :<|> b
infixr 8 :<|>
data (a :: k) :> (b :: *)
infixr 9 :>
data Capture (a :: *)
Мы определяем только четыре конструкции на нашем упрощенном языке:
-
A
Get a
представляет и конечную точку типаa
(вида*
). В сравнение с полным Servant, мы игнорируем здесь типы контента. Нам нужно тип данных только для спецификаций API. Сейчас есть прямо соответствующие значения, и, следовательно, дляGet
нет конструктора. -
С
a :<|> b
мы представляем выбор между двумя маршрутами. Опять же, нам не нужен конструктор, но оказывается, что мы будем использовать пару обработчиков для представления обработчика API с помощью:<|>
. Для вложенных приложений:<|>
мы получим вложенные пары обработчиков, которые выглядят несколько уродливо, используя стандартная нотация в Haskell, поэтому мы определяем:<|>
конструктор эквивалентен паре. -
С
item :> rest
мы представляем вложенные маршруты, гдеitem
является первым компонентом, аrest
- остальные компоненты. В нашем упрощенном DSL существует только две возможности дляitem
: строка уровня типа илиCapture
. Поскольку тип уровня строки имеют видSymbol
, но aCapture
, определенный ниже имеет вид*
, мы делаем первый аргумент:>
вид-полиморфный, так что оба варианта принимаются система рода Haskell. -
A
Capture a
представляет собой компонент маршрута, который захватывается, анализируется, а затем подвергается обработчику как параметр типаa
. В полном СервереCapture
имеет дополнительную строку в качестве параметра который используется для создания документации. Здесь мы опускаем строку.
Пример API
Теперь мы можем записать версию спецификации API из
вопрос, адаптированный к фактическим типам, имеющим место в Data.Time
, и
к нашему упрощенному DSL:
type MyAPI = "date" :> Get Day
:<|> "time" :> Capture TimeZone :> Get ZonedTime
Интерпретация как сервер
Самый интересный аспект - это, конечно, то, что мы можем сделать с API, и в основном это вопрос.
Слуга определяет несколько интерпретаций, но все они следуют аналогичная картина. Мы определим его здесь, что интерпретация как веб-сервер.
В Servant функция serve
принимает прокси для типа API
и обработчик, соответствующий типу API, в WAI Application
, который
по существу является функцией от HTTP-запросов к ответам. Что ж
абстрагироваться от веб-части здесь и определить
serve :: HasServer layout
=> Proxy layout -> Server layout -> [String] -> IO String
вместо.
Класс HasServer
, который мы определим ниже, имеет экземпляры
для всех различных конструкций DSL типа и, следовательно,
кодирует, что означает, что тип Haskell layout
может быть интерпретируемым
как тип API сервера.
Proxy
устанавливает соединение между типом и уровнем значения.
Он определяется как
data Proxy a = Proxy
и его единственная цель состоит в том, что, передавая конструктор Proxy
с явно указанным типом, мы можем сделать его очень явным
для какого типа API мы хотим вычислить сервер.
Аргумент Server
является обработчиком для API
. Здесь Server
сам является семейством типов и вычисляет из типа API тип
что обработчик должен иметь. Это один из основных компонентов того, что
делает Servant работать правильно.
Список строк представляет запрос, приведенный к списку
Компоненты URL. В результате мы всегда возвращаем ответ String
,
и мы допускаем использование IO
. Полный Слуга использует несколько больше
сложные типы здесь, но идея одна и та же.
Семейство типов Server
Сначала определяем Server
как семейство типов.
(В Servant используется фактическое семейство типов ServerT
, и это
определяется как часть класса HasServer
.)
type family Server layout :: *
Обработчик для конечной точки Get a
представляет собой просто действие IO
производя a
. (Еще раз, в полном коде Сервера, мы имеем
немного больше параметров, например, при создании ошибки.)
type instance Server (Get a) = IO a
Обработчик для a :<|> b
- это пара обработчиков, поэтому мы можем
определить
type instance Server (a :<|> b) = (Server a, Server b) -- preliminary
Но, как указано выше, для вложенных вхождений :<|>
это приводит
к вложенным парам, которые выглядят несколько лучше с инфиксной парой
конструктор, поэтому Servant вместо этого определяет эквивалентную
type instance Server (a :<|> b) = Server a :<|> Server b
Остается объяснить, как обрабатывается каждый из компонентов пути.
Литеральные строки в маршрутах не влияют на тип обработчик:
type instance Server ((s :: Symbol) :> r) = Server r
Однако захват означает, что обработчик ожидает дополнительный аргумент захваченного типа:
type instance Server (Capture a :> r) = a -> Server r
Вычисление типа обработчика примера API
Если разложить Server MyAPI
, получим
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
Итак, как и предполагалось, для сервера для нашего API требуется пара обработчиков, тот, который предоставляет дату, и тот, который, учитывая часовой пояс, обеспечивает время. Мы можем определить их прямо сейчас:
handleDate :: IO Day
handleDate = utctDay <$> getCurrentTime
handleTime :: TimeZone -> IO ZonedTime
handleTime tz = utcToZonedTime tz <$> getCurrentTime
handleMyAPI :: Server MyAPI
handleMyAPI = handleDate :<|> handleTime
Класс HasServer
Нам еще нужно реализовать класс HasServer
, который выглядит как
следующим образом:
class HasServer layout where
route :: Proxy layout -> Server layout -> [String] -> Maybe (IO String)
Задача функции route
почти похожа на serve
. Внутренне
мы должны отправить входящий запрос на правильный маршрутизатор. в
случае :<|>
, это означает, что мы должны сделать выбор между двумя
обработчики. Как мы можем сделать этот выбор? Простой вариант - позволить
route
для отказа, возвращая Maybe
. (Опять же, полный слуга
здесь несколько сложнее, а в версии 0.5 будет много
улучшенная стратегия маршрутизации.)
После определения route
мы можем легко определить serve
в терминах
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
Если ни один из маршрутов не соответствует, мы терпим неудачу с 404. В противном случае мы верните результат.
Примеры HasServer
Для конечной точки Get
мы определили
type instance Server (Get a) = IO a
поэтому обработчик - это действие IO, создающее a
, которое мы имеем
превратиться в String
. Для этой цели мы используем show
. В
фактическая реализация Servant, это преобразование обрабатывается
по машинам типов контента, и обычно будет включать кодирование
к JSON или 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
Поскольку мы сопоставляем только конечную точку, требуется запрос
в этот момент будет пустым. Если это не так, этот маршрут не
и возвращаем Nothing
.
Посмотрите на следующий выбор:
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
Здесь мы получаем пару обработчиков, и мы используем <|>
для Maybe
попробовать оба.
Что происходит для строковой строки?
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
Обработчик для s :> r
имеет тот же тип, что и обработчик для r
.
Мы требуем, чтобы запрос был непустым и первый компонент соответствовал
эквивалент уровня уровня строки типа. Мы получаем
строка уровня значения, соответствующая строковому литералу типа
применяя symbolVal
. Для этого нам нужно ограничение KnownSymbol
на
строковый литерал типа. Но все конкретные литералы в GHC
автоматически экземпляр KnownSymbol
.
Последний пример для захватов:
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
В этом случае мы можем предположить, что наш обработчик фактически является функцией, которая
ожидает a
. Мы требуем, чтобы первый компонент запроса обрабатывался
как a
. Здесь мы используем Read
, тогда как в Servant мы используем тип содержимого
машины снова. Если чтение не выполняется, мы считаем запрос не соответствовать.
В противном случае мы можем передать его обработчику и продолжить.
Тестирование всего
Теперь все готово.
Мы можем подтвердить, что все работает в 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)