如何提高在Haskell中使用JSON的易用性?
Haskell已经成为一种有用的web语言(感谢如何提高在Haskell中使用JSON的易用性?,json,haskell,aeson,Json,Haskell,Aeson,Haskell已经成为一种有用的web语言(感谢仆人!),但JSON对我来说仍然是那么痛苦,所以我一定是做错了什么(?) 我听说JSON是一个痛点,我听到的回答围绕“使用PureScript”、“等待子行/行类型”、“使用像乙烯基一样的深奥”、“Aeson+只处理锅炉板数据类型的爆炸”展开 作为一个(不公平的)参考点,我真的很喜欢Clojure的JSON“故事”(当然,它是一种动态语言,并且有我仍然更喜欢Haskell的权衡) 这是一个我已经看了一个小时的例子 { "access_tok
仆人!),但JSON对我来说仍然是那么痛苦,所以我一定是做错了什么(?)
我听说JSON是一个痛点,我听到的回答围绕“使用PureScript”、“等待子行/行类型”、“使用像乙烯基一样的深奥”、“Aeson+只处理锅炉板数据类型的爆炸”展开
作为一个(不公平的)参考点,我真的很喜欢Clojure的JSON“故事”(当然,它是一种动态语言,并且有我仍然更喜欢Haskell的权衡)
这是一个我已经看了一个小时的例子
{
"access_token": "xxx",
"batch": [
{"method":"GET", "name":"oldmsg", "relative_url": "<MESSAGE-ID>?fields=from,message,id"},
{"method":"GET", "name":"imp", "relative_url": "{result=oldmsg:$.from.id}?fields=impersonate_token"},
{"method":"POST", "name":"newmsg", "relative_url": "<GROUP-ID>/feed?access_token={result=imp:$.impersonate_token}", "body":"message={result=oldmsg:$.message}"},
{"method":"POST", "name":"oldcomment", "relative_url": "{result=oldmsg:$.id}/comments", "body":"message=Post moved to https://workplace.facebook.com/{result=newmsg:$.id}"},
{"method":"POST", "name":"newcomment", "relative_url": "{result=newmsg:$.id}/comments", "body":"message=Post moved from https://workplace.facebook.com/{result=oldmsg:$.id}"},
]
}
这是令人痛苦的僵化,处理可能
的一切都很不舒服。Nothing
是JSONnull
?还是应该缺席?然后我担心如何导出Aeson实例,并且必须弄清楚如何将egrelativeUrl
转换为relativeUrl
。然后我添加了一个端点,现在我有了名称冲突<代码>重复记录字段
!但是等等,这在其他地方引起了很多问题。因此,更新数据类型以使用egbatchReqRelativeUrl
,并在使用Typeable
s和Proxy
s派生实例时将其剥离。然后,我需要添加端点,或者调整我添加了更多数据点的僵化数据类型的形状,尽量不让“小差异的暴政”过多地膨胀我的数据类型
在这一点上,我主要使用JSON,因此决定使用lens
es来实现“动态”功能。因此,为了钻取包含组id的JSON字段,我做了以下操作:
filteredBy :: (Choice p, Applicative f) => (a -> Bool) -> Getting (Data.Monoid.First a) s a -> Optic' p f s s
filteredBy cond lens = filtered (\x -> maybe False cond (x ^? lens))
-- the group to which to move the message
groupId :: AsValue s => s -> AppM Text
groupId json = maybe (error500 "couldn't find group id in json.")
pure (json ^? l)
where l = changeValue . key "message_tags" . values . filteredBy (== "group") (key "type") . key "id" . _String
这对访问字段来说相当沉重。但我也需要产生有效载荷,我还没有足够的技能去看镜头如何适合这一点。围绕激励性批处理请求,我想出了一种“动态”的方法来编写这些有效负载。它可以用辅助fns简化,但是,我甚至不知道它会有多好
softMove :: Text -> Text -> Text -> Value
softMove accessToken msgId groupId = object [
"access_token" .= accessToken
, "batch" .= [
object ["method" .= String "GET", "name" .= String "oldmsg", "relative_url" .= String (msgId `append` "?fields=from,message,id")]
, object ["method" .= String "GET", "name" .= String "imp", "relative_url" .= String "{result=oldmsg:$.from.id}?fields=impersonate_token"]
, object ["method" .= String "POST", "name" .= String "newmsg", "relative_url" .= String (groupId `append` "/feed?access_token={result=imp:$.impersonate_token}"), "body" .= String "message={result=oldmsg:$.message}"]
, object ["method" .= String "POST", "name" .= String "oldcomment", "relative_url" .= String "{result=oldmsg:$.id}/comments", "body" .= String "message=Post moved to https://workplace.facebook.com/{result=newmsg:$.id}"]
, object ["method" .= String "POST", "name" .= String "newcomment", "relative_url" .= String "{result=newmsg:$.id}/comments", "body" .= String "message=Post moved from https://workplace.facebook.com/{result=oldmsg:$.id}"]
]
]
我正在考虑在代码中包含JSON blob或将它们作为文件读取,并使用Text.Printf
拼接变量
我的意思是,我可以这样做,但我很乐意找到一个替代方案。FB的API有点独特,因为它不能像许多REST API那样表示为一个僵硬的数据结构;他们称之为GraphAPI,它在使用中更具动态性,迄今为止,将其视为一个僵化的API是一件痛苦的事情
(同时,感谢所有社区的帮助,让我与Haskell走到了这一步!)更新:在底部添加了一些关于“动态战略”的评论
在类似的情况下,我使用了单字符帮助程序,效果很好:
json1 :: Value
json1 = o[ "batch" .=
[ o[ "method" .= s"GET", "name" .= s"oldmsg",
"url" .= s"..." ]
, o[ "method" .= s"POST", "name" .= s"newmsg",
"url" .= s"...", "body" .= s"..." ]
]
]
where o = object
s = String
请注意,非标准语法(一个字符助手和参数之间没有空格)是有意的。这对我和其他阅读我的代码的人来说是一个信号,它们是满足类型检查器的技术“注释”,而不是一种更常见的函数调用,实际上正在做一些事情
虽然这会增加一些混乱,但是在阅读代码时注释很容易被忽略。在编写代码时,它们也很容易忘记,但类型检查器会捕获它们,因此它们很容易修复
在您的特殊情况下,我认为一些更结构化的助手是有意义的。比如:
softMove :: Text -> Text -> Text -> Value
softMove accessToken msgId groupId = object [
"access_token" .= accessToken
, "batch" .= [
get "oldmsg" (msgId <> "?fields=from,message,id")
, get "imp" "{result=oldmsg:$.from.id}?fields=impersonate_token"
, post "newmsg" (groupId <> "...") "..."
, post "oldcomment" "{result=oldmsg:$.id}/comments" "..."
, post "newcomment" "{result=newmsg:$.id}/comments" "..."
]
]
where get name url = object $ req "GET" name url
post name url body = object $ req "POST" name url
<> ["body" .= s body]
req method name url = [ "method" .= s method, "name" .= s name,
"relative_url" .= s url ]
s = String
softMove::Text->Text->Text->Value
softMove accessToken msgId groupId=对象[
“访问令牌”。=accessToken
,“批次”。=[
获取“oldmsg”(msgId?字段=发件人、消息、id)
,获取“imp”“{result=oldmsg:$.from.id}?fields=impersonate_-token”
,张贴“newmsg”(groupId“…)”
,发布“oldcomment”“{result=oldmsg:$.id}/comments”“…”
,发布“newcommon”“{result=newmsg:$.id}/comments”“…”
]
]
其中get name url=object$req“get”name url
post name url body=对象$req“post”名称url
[body.=s body]
请求方法名称url=[“方法”..=s方法,“名称”..=s名称,
“相对url”。=s url]
s=字符串
注意,您可以根据特定情况下生成的特定JSON定制这些帮助程序,并在where
子句中本地定义它们。您不需要提交包含代码中所有JSON用例的大量ADT和函数基础设施,如果JSON在整个应用程序中的结构更加统一,您可能会这样做
关于“动态战略”的评论
关于使用“动态策略”是否是正确的方法,它可能取决于比在堆栈溢出问题中实际共享的更多的上下文。但是,退一步说,Haskell类型的系统在一定程度上是有用的,因为它有助于清晰地对问题领域进行建模。在最好的情况下,类型感觉自然,并帮助您编写正确的代码。当他们停止这样做时,你需要重新考虑你的类型
您在使用更传统的ADT驱动方法解决此问题时所遇到的痛苦(类型的僵化、可能的扩散以及“微小差异的暴政”)表明,这些类型是一个糟糕的模型,至少对于您在本例中尝试的操作而言是如此。,考虑到您的问题是为外部API生成相当简单的JSON指令/命令,而不是对碰巧也允许JSON序列化/反序列化的结构进行大量数据操作,因此将数据建模为Haskell ADT可能有些过分
我最好的猜测是,如果您真的想正确地为FBWorkplaceAPI建模,那么您不会希望在JSON级别进行建模。相反,您可以使用消息
、注释
、和组
类型在更高的抽象级别上执行此操作,并且最终希望动态生成JSON,因为您的类型
softMove :: Text -> Text -> Text -> Value
softMove accessToken msgId groupId = object [
"access_token" .= accessToken
, "batch" .= [
get "oldmsg" (msgId <> "?fields=from,message,id")
, get "imp" "{result=oldmsg:$.from.id}?fields=impersonate_token"
, post "newmsg" (groupId <> "...") "..."
, post "oldcomment" "{result=oldmsg:$.id}/comments" "..."
, post "newcomment" "{result=newmsg:$.id}/comments" "..."
]
]
where get name url = object $ req "GET" name url
post name url body = object $ req "POST" name url
<> ["body" .= s body]
req method name url = [ "method" .= s method, "name" .= s name,
"relative_url" .= s url ]
s = String