如何提高Haskell中使用JSON的便利性?

时间:2019-04-03 23:28:18

标签: json haskell aeson

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}"},
    ]
}

我需要将此帖子发布到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
  ...
  ]

这是非常痛苦的僵局,整个Maybe都很难应付。 Nothing是JSON null吗?还是应该缺席?然后,我担心要派生Aeson实例,因此不得不弄清楚如何将relativeUrl转换为relative_url。然后我添加了一个端点,现在有名称冲突。 DuplicateRecordFields!但是,等等,这在其他地方引起了很多问题。因此,更新数据类型以使用例如batchReqRelativeUrl,并在使用TypeableProxy派生实例时将其剥离。然后,我需要添加端点,或者调整要添加更多数据点的刚性数据类型的形状,以免让“细微差别的暴政”使我的数据类型过分膨胀。

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

访问字段非常繁重。但是我还需要生成有效载荷,而且我还没有足够的技巧来了解镜头如何做到这一点。围绕激励性批处理请求,我提出了一种“动态”的方式来编写这些有效负载。可以使用helper 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保持了联系!)

1 个答案:

答案 0 :(得分: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在整个应用程序的结构中更加统一一样。

对“动态策略”的评论

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

您使用更传统的ADT驱动的方法来解决此问题时遇到的痛苦(类型的刚性,Maybes的泛滥以及“细微差别的暴政”)表明这些类型是一个不好的模型< strong>至少对于您在这种情况下要执行的操作。特别是,鉴于您的问题是为外部API生成相当简单的JSON指令/命令之一,而不是对结构进行大量数据处理碰巧也允许JSON序列化/反序列化,因此将数据建模为Haskell ADT可能会过大。

我的最佳猜测是,如果您确实想正确地对FB Workplace API建模,则您不想在JSON级别上进行建模。相反,您将使用MessageCommentGroup类型在更高的抽象水平上进行操作,最终您还是想要动态地生成JSON,因为您的类型不会直接映射到API期望的JSON结构。

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

在较大的Web应用程序中使用这些程序包时,可以比HTML元素以更高的抽象级别对问题域进行建模,并且由于没有清晰的一对一映射,因此可以以动态方式生成HTML。在问题域模型的类型和HTML元素之间。

另一方面,有一个<span>包可以对单个元素进行建模,因此尝试将type-of-html嵌套在<tr>内是一种类型错误。等等。开发这些类型可能需要花费大量的工作,并且存在很多“僵化”的灵活性,但是要权衡的是类型安全的另一个整体。另一方面,对于HTML而言,这似乎比对特定的挑剔JSON API而言要容易。