Подтвердить что ты не робот

Каковы все механизмы, используемые для включения API типа Servant?

Я очень озадачен тем, как Слуга способен достичь волшебства, которое он делает, используя типизацию. Пример на веб-сайте уже сильно меня озадачивает:

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

Я получаю "дату", "время", [JSON] и "tz" - литералы уровня. Это значения, которые имеют "ставшие" типы. Хорошо.

Я получаю, что :> и :<|> являются операторами типа. Хорошо.

Я не понимаю, как эти вещи, после того как они стали типами, могут быть извлечены обратно в ценности. Какой механизм для этого?

Я также не понимаю, как первая часть этого типа может заставить структуру ожидать функцию подписи IO Date или как вторая часть этого типа может заставить фреймворк ожидать функцию подписи Timezone -> IO Time от меня. Как происходит это преобразование?

И как же тогда фреймворк может вызвать функцию, для которой он изначально не знал тип?

Я уверен, что здесь есть ряд расширений GHC и уникальных функций, которые я не знаю, чтобы объединить эту магию.

Может кто-нибудь объяснить, какие функции задействованы здесь и как они работают вместе?

4b9b3361

Ответ 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, но a Capture, определенный ниже имеет вид *, мы делаем первый аргумент :> вид-полиморфный, так что оба варианта принимаются система рода 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)