如何提高在Haskell中使用JSON的难易程度?

问题描述 投票:4回答:1

Haskell已经成为一种有用的网络语言(感谢Servant!),但JSON对我来说仍然是如此痛苦,所以我一定做错了什么(?)

我听到JSON被认为是一个痛点,我听到的回应围绕“使用PureScript”,“等待Sub / Row打字”,“使用esoterica,像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}"},
    ]
}

我需要将此POST发送到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
  ...
  ]

这是非常严重的,并且全身心地处理Maybes是不舒服的。 Nothing是JSON null吗?或者该领域是否缺席?然后我担心导出Aeson实例,并且必须弄清楚如何将relativeUrl转换为relative_url。然后我添加了一个端点,现在我有了名字冲突。 DuplicateRecordFields!但等等,这会在其他地方引起很多问题。因此,更新数据类型以使用例如batchReqRelativeUrl,并在使用Typeables和Proxys派生实例时将其剥离。然后我需要添加端点,或者按摩那些我添加了更多数据点的刚性数据类型的形状,试图不让“小差异的暴政”膨胀我的数据类型太多。

在这一点上,我主要消耗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

访问字段相当繁重。但我还需要产生有效载荷,而且我不够熟练,不知道镜头对它有多好处。围绕激励批处理请求,我想出了一种编写这些有效负载的“动态”方式。它可以用帮助器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那样表现为严格的数据结构;他们称之为Graph API,它在使用中更具动态性,并且将其视为刚性API一直令人痛苦。

(另外,感谢所有社区帮助让我与Haskell一起走了这么远!)

json haskell aeson
1个回答
3
投票

更新:在底部添加了一些关于“动态策略”的评论。

在类似的情况下,我使用单字符助手来达到很好的效果:

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

请注意,您可以根据特定情况下生成的特定JSON定制这些帮助程序,并在where子句中本地定义它们。您不需要提交大量的ADT和功能基础结构来覆盖代码中的所有JSON用例,就像JSON在整个应用程序中的结构更加统一一样。

Comments on the "Dynamic Strategy"

关于使用“动态策略”是否是正确的方法,它可能依赖于更多的上下文而不是在Stack Overflow问题中实际共享。但是,退一步,Haskell类型系统在有助于清楚地模拟问题域的范围内是有用的。在最好的情况下,类型感觉自然,并帮助您编写正确的代码。当他们停止这样做时,您需要重新考虑您的类型。

对于这个问题采用更传统的ADT驱动方法遇到的痛苦(类型的僵化,Maybes的扩散以及“小差异的暴政”)表明这些类型是一个糟糕的模型,至少对于你所尝试的在这种情况下。特别是,鉴于您的问题是为外部API生成相当简单的JSON指令/命令,而不是在允许JSON序列化/反序列化的结构上进行大量数据操作,将数据建模为Haskell ADT可能是过度杀伤。

我最好的猜测是,如果你真的想要正确建模FB Workplace API,你不希望在JSON级别上进行。相反,你可以使用MessageCommentGroup类型在更高的抽象层次上完成它,并且你最终还是想要动态生成JSON,因为你的类型不会直接映射到期望的JSON结构。 API。

将您的问题与生成HTML进行比较可能会很有见地。首先考虑lucid(基于blaze)或shakespeare模板包。如果你看一下这些是如何工作的,他们不会尝试通过生成带有像data Element = ImgElement ... | BlockquoteElement ...这样的ADT的DOM然后将它们序列化为HTML来构建HTML。据推测,作者认为这种抽象并不是必需的,因为HTML只需要生成,而不是分析。相反,他们使用函数(lucid)或quasiquoter(shakespeare)来构建表示HTML文档的动态数据结构。所选择的结构足够坚固以确保某些种类的有效性(例如,打开和关闭元素标签的正确匹配)而不是其他(例如,没有人阻止你在<p>元素的中间粘贴<span>孩子)。

当您在更大的Web应用程序中使用这些包时,您可以在比HTML元素更高的抽象级别对问题域进行建模,并且您以很大程度上动态的方式生成HTML,因为类型之间没有明确的一对一映射在您的问题域模型和HTML元素中。

另一方面,有一个type-of-html包对单个元素进行建模,因此尝试在<tr>中嵌套<td>等类型错误。开发这些类型可能需要做很多工作,并且有很多不灵活性“烘焙”,但权衡是另一个类型的安全性。另一方面,对于HTML来说,这似乎比为特定的挑剔的JSON API更容易。

© www.soinside.com 2019 - 2024. All rights reserved.