用于启用Servant基于类型的API的所有机制是什么?

时间:2015-10-31 20:06:44

标签: haskell types data-kinds

我对Servant如何通过打字实现它所带来的魔力感到非常困惑。网站上的例子已经让我非常困惑:

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

我得到&#34;日期&#34;,&#34;时间&#34;,[JSON]和&#34; tz&#34;是类型级文字。它们是具有&#34;成为&#34;类型。好。

我知道:>:<|>是类型运算符。好。

我不知道这些事物在成为类型之后如何被提取回价值观。这样做的机制是什么?

我也不知道这种类型的第一部分如何让框架期望签名IO Date的功能,或者这种类型的第二部分如何使框架期望a来自我的签名Timezone -> IO Time的功能。这种转变是如何发生的?

然后框架如何调用一个它最初不知道类型的函数?

我确定这里有许多GHC扩展和独特功能,我不熟悉这种结合使这种魔力发生。

有人可以解释一下这里涉及哪些功能以及它们如何协同工作?

1 个答案:

答案 0 :(得分:38)

查看Servant paper可获得完整说明 是最好的选择。尽管如此,我还是试着说明这种方法 由Servant在这里实施,通过实施&#34; TinyServant&#34;,一个版本的 仆人减少到最低限度。

很抱歉,这个答案太长了。但是,它仍然有点短 而不是纸张,这里讨论的代码是&#34;只有&#34; 81行, 也可用作Haskell文件here

制剂

首先,以下是我们需要的语言扩展程序:

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

类型级DSL的定义需要前三个 本身。 DSL使用类型级别的字符串(DataKinds) 使用类型多态(PolyKinds)。使用类型级中缀 :<|>:>等运营商需要TypeOperators 扩展

解释的定义需要第二个三个 (我们将定义一些让人想起Web服务器的功能,但是 没有整个网络部分)。为此,我们需要类型级函数 (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 :: *)

我们只用简化语言定义了四种结构:

  1. Get a表示类型a的类型和端点{类型*)。在 与完整的Servant比较,我们忽略了这里的内容类型。我们需要 仅适用于API规范的数据类型。现在有直接的 相应的值,因此Get没有构造函数。

  2. 使用a :<|> b,我们代表两条路线之间的选择。 同样,我们不需要构造函数,但事实证明 我们将使用一对处理程序来表示一个处理程序 使用:<|>的API。对于:<|>的嵌套应用程序,我们得到了 嵌套的处理程序对,使用它看起来有些难看 Haskell中的标准符号,因此我们定义:<|> 构造函数等价于一对。

  3. 使用item :> rest,我们代表嵌套路线,其中item 是第一个组件,rest是其余组件。 在我们简化的DSL中,只有两种可能性 item:类型级字符串或Capture。因为类型级别 字符串属于Symbol种类,但是Capture,定义如下 属于*,我们制作:>的第一个参数 kind-polymorphic ,以便接受这两个选项 Haskell类系统。

  4. Capture a表示捕获的路径组件, 解析然后作为类型a的参数暴露给处理程序。 在完整的Servant中,Capture有一个附加字符串作为参数 用于文档生成。我们在这里省略了字符串。

  5. 示例API

    我们现在可以从中记下API规范的一个版本 问题,适应Data.Time中发生的实际类型,和 到我们简化的DSL:

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

    解释为服务器

    最有趣的方面当然是我们可以做些什么 API,这也是问题的主要内容。

    Servant定义了几种解释,但它们都遵循a 类似的模式。我们在这里定义一个,其灵感来自于 解释为网络服务器。

    在Servant中,serve函数采用API类型的代理 以及将API类型与WAI Application匹配的处理程序 本质上是从HTTP请求到响应的函数。我们&#39; 11 这里抽象来自Web部分,并定义

    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正常工作。

    字符串列表表示请求,缩减为列表 网址组件。因此,我们始终返回String响应, 我们允许使用IO。 Full Servant使用更多 这里有复杂的类型,但想法是一样的。

    Server类型系列

    我们首先将Server定义为类型系列。 (在Servant中,使用的实际类型族是ServerT,它 被定义为HasServer类的一部分。)

    type family Server layout :: *
    

    Get a端点的处理程序只是一个IO操作 产生a。 (再次,在完整的Servant代码中,我们有 稍微多一些选项,例如产生错误。)

    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相似。在内部, 我们必须将传入的请求分派给正确的路由器。在里面 :<|>的情况,这意味着我们必须在两者之间做出选择 处理程序。我们如何做出这个选择?一个简单的选择是允许 返回routeMaybe失败。 (同样,完整的仆人是 这里有点复杂,而版本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
    

    所以处理程序是一个产生a的IO动作,我们有 变成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)
    
相关问题