我正在尝试为tutorial库gork servant,这是一种类型级别的网络DSL。该库广泛使用DataKind
语言扩展。
在该教程的早期,我们找到了以下定义Web服务端点的行:
type UserAPI = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User]
我不明白在类型签名中包含字符串和数组意味着什么。我也不清楚勾号('
)在'[JSON]
前面的含义。
所以我的问题归结为字符串和数组的类型/种类,以及later在转换为WAI结束点时如何解释?
作为旁注,在描述Nat
时Vect
和DataKinds
的一致使用会让我们在尝试理解这些内容时会看到令人沮丧的有限例子。我想我已经在不同的地方至少读了十几次这个例子,我仍然觉得我不明白发生了什么。
答案 0 :(得分:12)
我们的目标是仆人的目标:
API
我们的初始服务只是一个/
,它返回一个列表
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。
现在我们挑战自己设计一个路由系统,尽我们所能 在假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
服务,如果我们可以某种方式模式匹配将是很好的
为serve
,Be 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
请注意这里使用相关数据类型(这反过来要求我们
打开TypeFamilies
或GADTs
)。虽然是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 :> rest
到Content 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中调用函数让我们感到高兴
指定值参数的值,但不指定类型参数的类型。
悲伤的不对称。 undefined
和Proxy
都是编码方式
在值级别键入参数。 Proxy
能够做到没有
运行时成本和 t
中的Proxy t
是多边形的。 (undefined
类型为*
,因此undefined :: rest
甚至不会在这里检查。)
我们如何能够一路走向完整的Servant竞争对手?
我们需要将Be
分解为Get, Post, Put, Delete
。注意
其中一些动词现在也以请求的形式获取 in 数据
身体。在类型级别对内容类型和请求主体建模
需要类似的类型级机器。
如果用户想要将其功能建模为其他内容,该怎么办?
IO
,例如一堆monad变形金刚?
嘿,既然我们的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
,其中T
和U
是类似提升的T
和U
类型B
等编写'B
。Symbol
,因此您可以使用"foo" :: Symbol
和"bar" :: Symbol
作为有效类型。在您的示例中,"users"
和"sortby"
属于Symbol
种类型,JSON
类型为*
(旧式)类型(已定义here),'[JSON]
是类[*]
的类型,即它是单例类型级别列表(它以JSON ': '[]
相同的方式{{1}一般来说,它等同于[x]
。
x:[]
类型是[User]
种类的常规类型;它只是*
s列表的类型。它不是单例类型级列表。