如何提高在Haskell中使用JSON的易用性?

如何提高在Haskell中使用JSON的易用性?,json,haskell,aeson,Json,Haskell,Aeson,Haskell已经成为一种有用的web语言(感谢仆人!),但JSON对我来说仍然是那么痛苦,所以我一定是做错了什么(?) 我听说JSON是一个痛点,我听到的回答围绕“使用PureScript”、“等待子行/行类型”、“使用像乙烯基一样的深奥”、“Aeson+只处理锅炉板数据类型的爆炸”展开 作为一个(不公平的)参考点,我真的很喜欢Clojure的JSON“故事”(当然,它是一种动态语言,并且有我仍然更喜欢Haskell的权衡) 这是一个我已经看了一个小时的例子 { "access_tok

Haskell已经成为一种有用的web语言(感谢
仆人
!),但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
是JSON
null
?还是应该缺席?然后我担心如何导出Aeson实例,并且必须弄清楚如何将eg
relativeUrl
转换为
relativeUrl
。然后我添加了一个端点,现在我有了名称冲突<代码>重复记录字段
!但是等等,这在其他地方引起了很多问题。因此,更新数据类型以使用eg
batchReqRelativeUrl
,并在使用
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