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
,并在使用Typeable
和Proxy
派生实例时将其剥离。然后,我需要添加端点,或者调整要添加更多数据点的刚性数据类型的形状,以免让“细微差别的暴政”使我的数据类型过分膨胀。
在这一点上,我主要使用 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保持了联系!)
答案 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级别上进行建模。相反,您将使用Message
,Comment
和Group
类型在更高的抽象水平上进行操作,最终您还是想要动态地生成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而言要容易。