Jos*_*h.F 5 json haskell aeson
Haskell 作为一种网络语言已经变得很有用了(谢谢Servant!),但 JSON 对我来说仍然很痛苦,所以我一定是做错了什么(?)
我听到 JSON 被提及为一个痛点,我听到的回应围绕着“使用 PureScript”、“等待子/行输入”、“使用深奥的,如 Vinyl”、“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}"},
]
}
Run Code Online (Sandbox Code Playgroud)
我需要将其发布到 FB 工作场所,该工作场所会将消息复制到新组,并评论两个组上的链接,相互链接。
我的第一次尝试看起来像这样:
data BatchReq = BatchReq {
method :: Text
, name :: Text
, relativeUrl :: Text
, body :: Maybe Text
}
data BatchReqs = BatchReqs {
accessToken :: Text
, batch :: [BatchReq]
}
softMove tok msgId= BatchReqs tok [
BatchReq "GET" "oldmsg" (msgId `append` "?fields=from,message,id") Nothing
...
]
Run Code Online (Sandbox Code Playgroud)
这是非常僵化的,而且处理Maybe所有事情都让人不舒服。是NothingJSON吗null?或者该字段应该不存在?然后我担心派生 Aeson 实例,并且必须弄清楚如何将 eg 转换relativeUrl为relative_url. 然后我添加了一个端点,现在出现了名称冲突。DuplicateRecordFields!但是等等,这会在其他地方引起很多问题。因此,更新数据类型以使用 eg ,并在使用s 和sbatchReqRelativeUrl派生实例时将其剥离。然后我需要添加端点,或者调整刚性数据类型的形状,为此我添加了更多数据点,试图不让“微小差异的暴政”使我的数据类型过于膨胀。TypeableProxy
此时,我主要使用JSON,因此决定“动态”使用lenses。因此,为了深入研究包含组 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
Run Code Online (Sandbox Code Playgroud)
访问字段相当繁重。但我还需要生成有效负载,但我的技术不够熟练,无法了解镜头如何有效地实现这一点。围绕激励批处理请求,我想出了一种“动态”方式来编写这些有效负载。可以使用辅助 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}"]
]
]
Run Code Online (Sandbox Code Playgroud)
我正在考虑在代码中使用 JSON blob 或将它们作为文件读取并用于Text.Printf拼接变量...
我的意思是,我可以这样做,但肯定会很高兴找到替代方案。FB 的 API 有点独特,因为它不能像很多 REST API 那样表示为严格的数据结构;他们称其为图形 API,在使用中更加动态,到目前为止,将其视为严格的 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
Run Code Online (Sandbox Code Playgroud)
请注意,非标准语法(单字符帮助程序和参数之间没有空格)是故意的。这是向我和其他阅读我的代码的人发出的信号,表明这些是满足类型检查器的技术“注释”,而不是实际执行的更常见的函数调用某些操作的更常见的函数调用。
虽然这会增加一些混乱,但在阅读代码时很容易忽略注释。在编写代码时它们也很容易被忘记,但是类型检查器会捕获它们,因此它们很容易修复。
在你的具体情况下,我认为一些更有条理的助手确实有意义。就像是:
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
Run Code Online (Sandbox Code Playgroud)
请注意,您可以根据在特定情况下生成的特定JSON定制这些帮助程序,并在本地定义它们where,并在子句中本地定义它们。您无需致力于覆盖代码中所有 JSON 用例的 ADT 和功能基础设施的大块,如果 JSON 在整个应用程序中的结构更加统一,您可能会这样做。
关于使用“动态策略”是否是正确的方法,它可能取决于比 Stack Overflow 问题中实际共享的更多上下文。但是,退一步来说,Haskell 类型系统的有用之处在于它有助于清晰地对问题域进行建模。在最好的情况下,这些类型感觉很自然,可以帮助您编写正确的代码。当他们停止这样做时,您需要重新考虑您的类型。
您在使用更传统的 ADT 驱动方法解决此问题时遇到的痛苦(类型的僵化、 的扩散Maybes以及“小差异的暴政”)表明这些类型至少对于您尝试做的事情来说是一个糟糕的模型在这种情况下。 特别是,考虑到您的问题是为外部 API 生成相当简单的 JSON 指令/命令,而不是对也恰好允许 JSON 序列化/反序列化的结构进行大量数据操作,将数据建模为 Haskell ADT 可能有点矫枉过正。
我最好的猜测是,如果您确实想正确建模 FB Workplace API,您不会希望在 JSON 级别上进行。相反,您可以使用Message、Comment和Grouptypes 在更高的抽象级别上执行此操作,并且您最终会希望动态生成 JSON,因为您的类型不会直接映射到 API 期望的 JSON 结构。
将您的问题与生成 HTML 进行比较可能会很有洞察力。首先考虑lucid(blaze基于)或shakespeare模板包。如果你看看它们是如何工作的,你会发现它们不会尝试通过使用 ADT 生成 DOM 来构建 HTML data Element = ImgElement ... | BlockquoteElement ...,然后将它们序列化为 HTML。想必作者认为这种抽象并不是真正必要的,因为只需要生成 HTML ,而不需要分析。相反,他们使用函数 ( lucid) 或准引用符 ( shakespeare) 来构建表示 HTML 文档的动态数据结构。所选择的结构足够严格,可以确保某些类型的有效性(例如,开始和结束元素标签的正确匹配),但不能确保其他类型的有效性(例如,没有人阻止您将子元素放在元素<p>中间<span>)。
当您在较大的 Web 应用程序中使用这些包时,您可以在比 HTML 元素更高的抽象级别上对问题域进行建模,并且以很大程度上动态的方式生成 HTML,因为类型之间没有明确的一对一映射在您的问题域模型和 HTML 元素中。
另一方面,有一个type-of-html包可以对各个元素进行建模,因此尝试<tr>在内部嵌套 a<td>等等会导致类型错误。开发这些类型可能需要花费大量的工作,并且存在很多“固有的”不灵活性,但权衡是完全不同级别的类型安全性。另一方面,这对于 HTML 来说似乎比对于特定的挑剔 JSON API 更容易。