我对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扩展和独特功能,我不熟悉这种结合使这种魔力发生。
有人可以解释一下这里涉及哪些功能以及它们如何协同工作?
答案 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规范。
data Get (a :: *)
data a :<|> b = a :<|> b
infixr 8 :<|>
data (a :: k) :> (b :: *)
infixr 9 :>
data Capture (a :: *)
我们只用简化语言定义了四种结构:
Get a
表示类型a
的类型和端点{类型*
)。在
与完整的Servant比较,我们忽略了这里的内容类型。我们需要
仅适用于API规范的数据类型。现在有直接的
相应的值,因此Get
没有构造函数。
使用a :<|> b
,我们代表两条路线之间的选择。
同样,我们不需要构造函数,但事实证明
我们将使用一对处理程序来表示一个处理程序
使用:<|>
的API。对于:<|>
的嵌套应用程序,我们得到了
嵌套的处理程序对,使用它看起来有些难看
Haskell中的标准符号,因此我们定义:<|>
构造函数等价于一对。
使用item :> rest
,我们代表嵌套路线,其中item
是第一个组件,rest
是其余组件。
在我们简化的DSL中,只有两种可能性
item
:类型级字符串或Capture
。因为类型级别
字符串属于Symbol
种类,但是Capture
,定义如下
属于*
,我们制作:>
的第一个参数
kind-polymorphic ,以便接受这两个选项
Haskell类系统。
Capture a
表示捕获的路径组件,
解析然后作为类型a
的参数暴露给处理程序。
在完整的Servant中,Capture
有一个附加字符串作为参数
用于文档生成。我们在这里省略了字符串。
我们现在可以从中记下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
如果我们展开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
所以处理程序是一个产生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)