在Servant库中解密DataKind类型提升

时间:2016-05-04 01:11:59

标签: haskell data-kinds servant

我正在尝试为tutorial库gork servant,这是一种类型级别的网络DSL。该库广泛使用DataKind语言扩展。

在该教程的早期,我们找到了以下定义Web服务端点的行:

type UserAPI = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User]

我不明白在类型签名中包含字符串和数组意味着什么。我也不清楚勾号(')在'[JSON]前面的含义。

所以我的问题归结为字符串和数组的类型/种类,以及later在转换为WAI结束点时如何解释?

作为旁注,在描述NatVectDataKinds的一致使用会让我们在尝试理解这些内容时会看到令人沮丧的有限例子。我想我已经在不同的地方至少读了十几次这个例子,我仍然觉得我不明白发生了什么。

2 个答案:

答案 0 :(得分:12)

让我们建立一个仆人

目标

我们的目标是仆人的目标:

  • 将我们的REST API指定为单一类型API
  • 将服务实现为单一副作用(读取:monadic) 功能
  • 使用真实类型对资源建模,仅序列化为较小的类型 在最后,例如JSON或字节串
  • 最常用的是常用的WAI(Web应用程序接口)界面 Haskell HTTP框架使用

越过门槛

我们的初始服务只是一个/,它返回一个列表 JSON中的User

-- Since we do not support HTTP verbs yet we will go with a Be
data User = ...
data Be a
type API = Be [User]

虽然我们还没有编写一行价值级代码,但我们已经有了 已经足够代表我们的REST服务 - 我们很简单 被欺骗并在类型级别完成。这让我们感到兴奋, 很长一段时间以来,我们第一次对网络抱有希望 再次编程。

我们需要一种方法将其转换为WAI type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived。 没有足够的空间来描述WAI的工作原理。基础知识:我们是 给定一个请求对象和一种构造响应对象的方法,我们 预计会返回一个响应对象。有很多方法 这样做,但这是一个简单的选择。

imp :: IO [User]
imp =
  return [ User { hopes = ["ketchup", "eggs"], fears = ["xenophobia", "reactionaries"] }
         , User { hopes = ["oldies", "punk"], fears = ["half-tries", "equivocation"] }
         ]

serve :: ToJSON a => Be a -> IO a -> Application
serve _ contentIO = \request respond -> do
  content <- contentIO
  respond (responseLBS status200 [] (encode content))

main :: IO ()
main = run 2016 (serve undefined imp)

这实际上有效。我们可以运行它并卷曲它并得到它 预期的回应。

% curl 'http://localhost:2016/'
[{"fears":["xenophobia","reactionaries"],"hopes":["ketchup","eggs"]},{"fears":["half-tries","equivocation"],"hopes":["oldies","punk"]}]%

请注意,我们从未构建类型为Be a的值。我们用了 undefined。该函数本身完全忽略该参数。 实际上,没有办法构建Be a类型的值 我们从未定义任何数据构造函数。

为什么甚至有Be a参数?可怜的简单事实是我们需要的 那个a变量。它告诉我们我们的内容类型是什么,以及它 让我们建立甜蜜的艾森约束。

代码:0Main.hs

:路上的&lt; |&gt;

现在我们挑战自己设计一个路由系统,尽我们所能 在假URL文件夹中的不同位置具有单独的资源 层次结构。我们的目标是支持这种类型的服务:

type API =
       "users" :> Be [User]
  :<|> "temperature" :> Int

为此,我们首先需要打开TypeOperators和。{ DataKinds个扩展程序。详见@ Cactus的答案,数据种类 允许我们在类型级别存储数据,GHC内置于 类型级别的字符串文字。 (自从定义字符串以来,这很棒 类型级别不是我的乐趣。)

(我们还需要PolyKinds所以GHC可以推断这种类型。是的,我们 现在深入丛林丛林的核心。)

然后,我们需要为:>(子目录)提供聪明的定义 operator)和:<|>(析取运算符)。

data path :> rest
data left :<|> right =
  left :<|> right

infixr 9 :>
infixr 8 :<|>

我说聪明吗?我的意思是死的简单。请注意,我们已经给出了 :<|>一个类型构造函数。这是因为我们将胶合我们的monadic 一起工作以实现分离......哦,它只是 更容易举个例子。

imp :: IO [User] :<|> IO Int
imp =
  users :<|> temperature
  where
    users =
      return [ User ["ketchup", "eggs"] ["xenophobia", "reactionaries"]
             , User ["oldies", "punk"] ["half-tries", "equivocation"]
             ]
    temperature =
      return 72

现在让我们把注意力转向serve的特殊问题。没有 我们可以写一个依赖于API的函数serve Be a。现在我们在RESTful的类型级别上有一个小DSL 服务,如果我们可以某种方式模式匹配将是很好的 为serveBe a键入并实施不同的path :> rest, 和left :<|> right。还有!

class ToApplication api where
  type Content api
  serve :: api -> Content api -> Application

instance ToJSON a => ToApplication (Be a) where
  type Content (Be a) = IO a
  serve _ contentM = \request respond -> do
    content <- contentM
    respond . responseLBS status200 [] . encode $ content

请注意这里使用相关数据类型(这反过来要求我们 打开TypeFamiliesGADTs)。虽然是Be a端点 有一个IO a类型的实现,这是不够的 实施脱节。作为薪水不足和懒惰的功能程序员我们 将简单地抛出另一层抽象并定义一个类型级别 名为Content的函数,它接受类型api并返回一个类型 Content api

instance Exception RoutingFailure where

data RoutingFailure =
  RoutingFailure
  deriving (Show)

instance (KnownSymbol path, ToApplication rest) => ToApplication (path :> rest) where
  type Content (path :> rest) = Content rest
  serve _ contentM = \request respond -> do
    case pathInfo request of
      (first:pathInfoTail)
        | view unpacked first == symbolVal (Proxy :: Proxy path) -> do
            let subrequest = request { pathInfo = pathInfoTail }
            serve (undefined :: rest) contentM subrequest respond
      _ ->
        throwM RoutingFailure

我们可以在这里细分代码:

  • 如果是ToApplication,我们保证path :> rest个实例 编译器可以保证path是一个类型级符号(意思是 它[除其他外]可以映射到String symbolVal ToApplication rest存在。

  • 当请求到达时,我们将pathInfos上的模式匹配为 确定成功。失败后,我们会做懒惰的事情并抛出 IO中未经检查的例外情况。

  • 成功后,我们将在类型级别递归(提示激光噪音) 和{雾机)与serve (undefined :: rest)。请注意rest 是一个更小的&#34;类型比path :> rest,非常类似于你 模式匹配数据构造函数,你最终得到一个&#34;较小的&#34; 值。

  • 在递归之前,我们方便地减少HTTP请求 记录更新。

请注意:

  • type Content功能图path :> restContent rest。 类型级别的另一种递归形式!还要注意这一点 意味着路线中的额外路径不会改变路径的类型 资源。这符合我们的直觉。

  • 在IO中抛出异常不是Great Library Design™,但我会 由你来解决这个问题。 (暗示: ExceptT / throwError。)

  • 希望我们在这里慢慢推动使用DataKinds 用字符串符号。能够在类型中表示字符串 level使我们能够使用类型来匹配路由 类型级别。

  • 我使用镜头打包和打开包装。我破解的速度更快 这些SO答案与镜头,但当然你可以使用 来自pack库的Data.Text

好的。还有一个例子。呼吸。休息一下。

instance (ToApplication left, ToApplication right) => ToApplication (left :<|> right) where
  type Content (left :<|> right) = Content left :<|> Content right
  serve _ (leftM :<|> rightM) = \request respond -> do
    let handler (_ :: RoutingFailure) =
          serve (undefined :: right) rightM request respond
    catch (serve (undefined :: left) leftM request respond) handler

在这个例子中我们

  • 如果编译器可以保证ToApplication (left :<|> right) 保证等等等等等等。

  • type Content函数中引入另一个条目。这里是 代码行,它允许我们构建一个IO [User] :<|> IO Int类型,并让编译器在过程中成功地将其分解 实例解析。

  • 抓住我们抛出的异常!当发生异常时 离开了,我们走向右边。同样,这不是Great Library Design™。

运行1Main.hs,您应该可以curl这样。

% curl 'http://localhost:2016/users'
[{"fears":["xenophobia","reactionaries"],"hopes":["ketchup","eggs"]},{"fears":["half-tries","equivocation"],"hopes":["oldies","punk"]}]%

% curl 'http://localhost:2016/temperature'
72%

给予和接受

现在让我们演示类型级列表的用法,这是另一个特性 DataKinds。我们将扩充data Be以存储类型列表 端点可以给出。

data Be (gives :: [*]) a

data English
data Haskell
data JSON

-- | The type of our RESTful service
type API =
       "users" :> Be [JSON, Haskell] [User]
  :<|> "temperature" :> Be [JSON, English] Int

让我们定义一个匹配类型列表的类型类 端点可以提供HTTP请求的MIME类型列表 可以接受。我们将在此处使用Maybe来表示失败。再一次,不是 伟大的图书馆设计™。

class ToBody (gives :: [*]) a where
  toBody :: Proxy gives -> [ByteString] -> a -> Maybe ByteString

class Give give a where
  give :: Proxy give -> [ByteString] -> a -> Maybe ByteString

为什么有两个单独的类型类?好吧,我们需要一个类[*], 这是类型列表的类型,以及类型*的类型 只是一种类型。就像你无法定义一样 一个参数的函数,它既是一个列表又是一个列表 一个非列表(因为它不会进行类型检查),我们无法定义类型类 这需要一个属于类型级列表的参数 和一个类型级别的非列表(因为它不会检查)。只要我们有 kindclasses ...

让我们看看这个类型类的实际应用:

instance (ToBody gives a) => ToApplication (Be gives a) where
  type Content (Be gives a) = IO a
  serve _ contentM = \request respond -> do
    content <- contentM
    let accepts = [value | ("accept", value) <- requestHeaders request]
    case toBody (Proxy :: Proxy gives) accepts content of
      Just bytes ->
        respond (responseLBS status200 [] (view lazy bytes))
      Nothing ->
        respond (responseLBS status406 [] "bad accept header")

非常好。我们使用toBody作为抽象计算的方法 将类型a的值转换为WAI的基础字节 想。如果失败,我们将简单地用406错误,其中一个更多 深奥(因此使用起来更有趣)状态代码。

但是等等,为什么首先使用类型级列表呢?因为 正如我们之前所做的那样,我们将在两者上进行模式匹配 建设者:零和缺点。

instance ToBody '[] a where
  toBody Proxy _ _ = Nothing

instance (Give first a, ToBody rest a) => ToBody (first ': rest) a where
  toBody Proxy accepted value =
    give (Proxy :: Proxy first) accepted value
      <|> toBody (Proxy :: Proxy rest) accepted value

希望这种方式有道理。列表运行时发生故障 在我们找到匹配之前是空的; <|>保证我们会短路 成功; toBody (Proxy :: Proxy rest)是递归案例。

我们需要一些有趣的Give个实例来玩。

instance ToJSON a => Give JSON a where
  give Proxy accepted value =
    if elem "application/json" accepted then
      Just (view strict (encode value))
    else
      Nothing

instance (a ~ Int) => Give English a where
  give Proxy accepted value =
    if elem "text/english" accepted then
      Just (toEnglish value)
    else
      Nothing
    where
      toEnglish 0 = "zero"
      toEnglish 1 = "one"
      toEnglish 2 = "two"
      toEnglish 72 = "seventy two"
      toEnglish _ = "lots"

instance Show a => Give Haskell a where
  give Proxy accepted value =
    if elem "text/haskell" accepted then
      Just (view (packed . re utf8) (show value))
    else
      Nothing

再次运行服务器,您应该能够curl这样:

% curl -i 'http://localhost:2016/users' -H 'Accept: application/json'
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Wed, 04 May 2016 06:56:10 GMT
Server: Warp/3.2.2

[{"fears":["xenophobia","reactionaries"],"hopes":["ketchup","eggs"]},{"fears":["half-tries","equivocation"],"hopes":["oldies","punk"]}]%

% curl -i 'http://localhost:2016/users' -H 'Accept: text/plain'
HTTP/1.1 406 Not Acceptable
Transfer-Encoding: chunked
Date: Wed, 04 May 2016 06:56:11 GMT
Server: Warp/3.2.2

bad accept header%

% curl -i 'http://localhost:2016/users' -H 'Accept: text/haskell'
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Wed, 04 May 2016 06:56:14 GMT
Server: Warp/3.2.2

[User {hopes = ["ketchup","eggs"], fears = ["xenophobia","reactionaries"]},User {hopes = ["oldies","punk"], fears = ["half-tries","equivocation"]}]%

% curl -i 'http://localhost:2016/temperature' -H 'Accept: application/json'
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Wed, 04 May 2016 06:56:26 GMT
Server: Warp/3.2.2

72%

% curl -i 'http://localhost:2016/temperature' -H 'Accept: text/plain'
HTTP/1.1 406 Not Acceptable
Transfer-Encoding: chunked
Date: Wed, 04 May 2016 06:56:29 GMT
Server: Warp/3.2.2

bad accept header%

% curl -i 'http://localhost:2016/temperature' -H 'Accept: text/english'
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Wed, 04 May 2016 06:56:31 GMT
Server: Warp/3.2.2

seventy two%

万岁!

请注意,我们已停止使用undefined :: t并切换为Proxy :: Proxy t。两者都是黑客。在Haskell中调用函数让我们感到高兴 指定值参数的值,但不指定类型参数的类型。 悲伤的不对称。 undefinedProxy都是编码方式 在值级别键入参数。 Proxy能够做到没有 运行时成本 t中的Proxy t是多边形的。 (undefined 类型为*,因此undefined :: rest甚至不会在这里检查。)

剩余工作

我们如何能够一路走向完整的Servant竞争对手?

  • 我们需要将Be分解为Get, Post, Put, Delete。注意 其中一些动词现在也以请求的形式获取 in 数据 身体。在类型级别对内容类型和请求主体建模 需要类似的类型级机器。

  • 如果用户想要将其功能建模为其他内容,该怎么办? IO,例如一堆monad变形金刚?

  • A more precise, yet more complicated, routing algorithm.

  • 嘿,既然我们的API有一个类型,那么就可以了 生成服务的客户端?制作HTTP的东西 请求服务遵守API描述而不是 创建HTTP服务本身?

  • 文档。确保每个人都了解所有这些 类型级别的hijinks是。 ;)

那个刻度线

  

我也不清楚刻度线(&#39;)在&#39; [JSON]前的含义。

答案晦涩难懂,陷入GHC's manual in section 7.9

  

由于构造函数和类型共享相同的命名空间,因此通过提升可以获得不明确的类型名称。在这些情况下,如果要引用提升的构造函数,则应在其名称前加上引号。

     

使用-XDataKinds,Haskell的列表和元组类型本身被提升为种类,并且在类型级别享受相同的方便语法,尽管前缀为引号。对于两个或多个元素的类型级列表,例如上面的foo2的签名,可以省略引用,因为含义是明确的。但是对于一个或零个元素的列表(如在foo0和foo1中),引用是必需的,因为类型[]和[Int]在Haskell中具有现有含义。

这就是我们上面编写的所有代码的冗长程度,除此之外还有很多原因是由于类型级编程仍然是Haskell中的二等公民,而不是依赖类型语言(Agda,Idris,Coq) )。语法很奇怪,扩展很多,文档稀疏,错误都是废话,但男孩哦男孩类型级编程很有趣。

答案 1 :(得分:3)

启用DataKinds后,您将获得基于常规数据类型定义自动创建的新类型:

  • 如果您有data A = B T | C U,现在可以获得新的A种新类型'B :: T -> A'C :: U -> A,其中TU是类似提升的TU类型
  • 的新类型
  • 如果没有歧义,您可以为B等编写'B
  • 类型级别的字符串共享相同类型的Symbol,因此您可以使用"foo" :: Symbol"bar" :: Symbol作为有效类型。

在您的示例中,"users""sortby"属于Symbol种类型,JSON类型为*(旧式)类型(已定义here),'[JSON]是类[*]的类型,即它是单例类型级别列表(它以JSON ': '[]相同的方式{{1}一般来说,它等同于[x]

x:[]类型是[User]种类的常规类型;它只是* s列表的类型。它不是单例类型级列表。