Aeson包中的decode和decode'函数有什么区别?

16

aeson包中的decodedecode'函数几乎相同,但它们在文档中有微妙的差异(这里仅发布有趣的文档部分):

-- This function parses immediately, but defers conversion.  See
-- 'json' for details.
decode :: (FromJSON a) => L.ByteString -> Maybe a
decode = decodeWith jsonEOF fromJSON

-- This function parses and performs conversion immediately.  See
-- 'json'' for details.
decode' :: (FromJSON a) => L.ByteString -> Maybe a
decode' = decodeWith jsonEOF' fromJSON

我尝试阅读jsonjson'函数的描述,但仍然不理解应该在哪种情况下使用哪个函数,因为文档不够清晰。是否有人可以更精确地描述两个函数之间的区别,并提供一些行为解释的示例(如果可能的话)?

更新:

还有decodeStrictdecodeStrict'函数。我不是在问decode'decodeStrict之间的区别是什么,例如这是一个有趣的问题。但所有这些函数中的惰性和严格性都不是很明显。


2
这并不回答你的问题,但是aeson上有一个开放的问题询问这个差异。 - Li-yao Xia
@Li-yaoXia 很好的发现!我没有发现那个问题。 - Shersh
aeson的源代码来看,唯一的区别似乎是字符串和数字是否会被完全强制(从而分配可能昂贵的数字或字符串数据结构)。话虽如此,我还没有能够触发这种行为... - Alec
我认为查看valuevalue'的意图是为了严格版本能够急切地构建任何嵌套的对象/数组。实际上,我不确定懒惰版本是否避免了很多工作,因为解析器必须检查是否有任何形成良好的对象/数组,然后才能移动到下一个属性,但我认为在懒惰版本中避免/推迟了在objectValues/arrayValues中构建HashMap/Vector的复制/处理。 - ryachza
2个回答

15
这两者之间的区别微妙。虽然有差异,但有点复杂。我们可以从类型入手。 Value 类型很重要的一点是,aeson 提供的该类型自版本 0.4.0.0 以来就一直是 strict 的。这意味着在 Value 的构造函数和其内部表示之间不能有任何 thunks。这立即意味着当一个 Value 被评估为 WHNF 时,Bool(当然还有 Null)必须被完全评估。
接下来,让我们考虑 StringNumberString 构造函数包含一个类型为 strictText 值,因此也不能有任何懒惰。同样,Number 构造函数包含一个 Scientific 值,该值由两个严格的值内部表示。当一个 Value 被评估为 WHNF 时,StringNumber 都必须被完全评估。
我们现在可以关注 JSON 提供的唯一非平凡数据类型 ObjectArray。它们更有趣。在 aeson 中,Object 由一个 惰性HashMap 表示。惰性的 HashMap 只评估其键到 WHNF,而不是其值,因此这些值可能仍然是未评估的thunks。同样,ArrayVector,它们的值也不是严格的。这两种类型的 Value 都可以包含 thunks。
有了这个想法,我们知道,一旦我们有了一个 Valuedecodedecode' 唯一可能不同的地方就在于对象和数组的生成。

观察差异

下一步,我们可以尝试在 GHCi 中实际评估一些内容并查看发生了什么。我们将从一堆导入和定义开始:
:seti -XOverloadedStrings

import Control.Exception
import Control.Monad
import Data.Aeson
import Data.ByteString.Lazy (ByteString)
import Data.List (foldl')
import qualified Data.HashMap.Lazy as M
import qualified Data.Vector as V

:{
forceSpine :: [a] -> IO ()
forceSpine = evaluate . foldl' const ()
:}

接下来,让我们实际解析一些JSON:

let jsonDocument = "{ \"value\": [1, { \"value\": [2, 3] }] }" :: ByteString

let !parsed = decode jsonDocument :: Maybe Value
let !parsed' = decode' jsonDocument :: Maybe Value
force parsed
force parsed'

现在我们有两个绑定变量,parsedparsed',其中一个使用decode进行解析,另一个使用decode'进行解析。它们被强制转换为WHNF,因此我们至少可以看到它们是什么,但是我们可以使用GHCi中的:sprint命令来查看每个值实际上被评估了多少。
ghci> :sprint parsed
parsed = Just _
ghci> :sprint parsed'
parsed' = Just
            (Object
               (unordered-containers-0.2.8.0:Data.HashMap.Base.Leaf
                  15939318180211476069 (Data.Text.Internal.Text _ 0 5)
                  (Array (Data.Vector.Vector 0 2 _))))

请看这个!使用decode解析的版本仍未评估,但使用decode'解析的版本具有一些数据。这使我们发现了两者之间的第一个有意义的区别:decode'强制其立即结果为WHNF,但decode将其推迟到需要时再进行评估。

让我们深入了解这些值,看看是否能找到更多的区别。一旦我们评估这些外部对象会发生什么?

let (Just outerObjValue) = parsed
let (Just outerObjValue') = parsed'
force outerObjValue
force outerObjValue'

ghci> :sprint outerObjValue
outerObjValue = Object
                  (unordered-containers-0.2.8.0:Data.HashMap.Base.Leaf
                     15939318180211476069 (Data.Text.Internal.Text _ 0 5)
                     (Array (Data.Vector.Vector 0 2 _)))

ghci> :sprint outerObjValue'
outerObjValue' = Object
                   (unordered-containers-0.2.8.0:Data.HashMap.Base.Leaf
                      15939318180211476069 (Data.Text.Internal.Text _ 0 5)
                      (Array (Data.Vector.Vector 0 2 _)))

这很明显。我们明确强制了两个对象,因此它们现在都被评估为哈希图。真正的问题是它们的元素是否被评估。

let (Array outerArr) = outerObj M.! "value"
let (Array outerArr') = outerObj' M.! "value"
let outerArrLst = V.toList outerArr
let outerArrLst' = V.toList outerArr'

forceSpine outerArrLst
forceSpine outerArrLst'

ghci> :sprint outerArrLst
outerArrLst = [_,_]

ghci> :sprint outerArrLst'
outerArrLst' = [Number (Data.Scientific.Scientific 1 0),
                Object
                  (unordered-containers-0.2.8.0:Data.HashMap.Base.Leaf
                     15939318180211476069 (Data.Text.Internal.Text _ 0 5)
                     (Array (Data.Vector.Vector 0 2 _)))]

另一个区别在于,使用decode解码的数组值并不是强制的,但是使用decode'解码的数组值是强制的。这意味着,正如文档所说的那样,“延迟转换”,decode直到实际需要时才会将其转换为Haskell值。

影响

显然,这两个函数略有不同,而且显然,decode'decode更严格。不过,有什么有意义的区别吗?你何时会更喜欢其中之一?

值得一提的是,decode永远不会比decode'做更多的工作,因此decode可能是正确的默认设置。当然,decode'也永远不会比decode做更多的工作,因为在产生任何值之前,整个JSON文档都需要被解析。唯一的显著区别是,如果只使用JSON文档的一小部分,则decode避免了分配Value

当然,惰性也不是免费的。惰性意味着添加thunks,这可能会消耗空间和时间。如果所有的thunks都将被评估,那么decode只是在浪费内存和运行时添加无用的间接引用。

在这个意义上,您可能想要使用decode'的情况是整个Value结构将被强制执行的情况,这可能取决于您使用的FromJSON实例。一般来说,除非性能真的很重要并且您正在解码大量JSON或在紧密循环中进行JSON解码,否则不用担心它们之间的选择。在任何情况下,您都应该进行基准测试。在decodedecode'之间进行选择是一种非常具体的手动优化,如果没有基准测试,我不会非常自信地认为其中任何一个可以改善程序的运行时特性。

我本来也想给出相同的答案,但不确定,然而我认为你是正确的。我的印象是如果你解码到比“Value”占用更多内存的其他对象,那么这也可能会产生很大的差异。例如,假设你有一个对象,它的值是经过游程编码的数组:“{"data1": [10000, 0],"data2": [1, 1]}”。如果你只访问“data2”,你不希望解压整个“data1”的值。 - Jon Purdy
@JonPurdy 我最初也是这么想的,我本来想加上一些关于这个的内容,但我意识到实际上它完全取决于 FromJSON 实例希望变得多么懒惰。最终,decodedecode' 都会产生一个 Value,然后将其提供给 fromJSON,随后 fromJSON 会生成任何 Parser 所产生的值。我不认为严格生成 Value 会在任何重要的方式上改变 fromJSON 所产生的值的严格性属性。 - Alexis King
1
我认为惰性只有在有效地延迟一堆切片转换为实际的“Text”值时才有潜在的好处。要通过延迟解析后的结果构建来获胜,结果必须比表示它们的thunk大得多。这通常是一个很高的要求。更糟糕的是,延迟构建可能会导致空间泄漏;只有一个未强制执行的结果就可以轻松地保持一个bytestring块的活动状态。 - dfeuer
我曾经处理过基本上是一个带有一堆元数据字段的对象包装器和一个包含大量复杂对象数组的JSON值。需要处理这些外部对象的大量集合且仅关注元数据并不罕见,所以我能看出这种区别可能会产生重大影响;你仍然需要识别JSON结构,以知道你忽略的数组何时结束并开始处理下一组元数据,但花时间将表示数组的JSON文本转换为嵌套的Haskell值将是浪费的。 - Ben
1
@AlexisKing,非常感谢您提供如此详细的解释和付出的努力!现在我对差异非常清楚了。我想知道,这是否可以以某种方式添加到aeson软件包的文档中,以回应我在评论中提出的问题? - Shersh
显示剩余2条评论

1
Haskell是一种惰性语言。当您调用函数时,它实际上不会立即执行,而是有关调用的信息被“记住”并返回到堆栈(文档中称此记住的调用信息为“thunk”),只有在堆栈上的某个人尝试对返回值执行操作时才会发生实际调用。
这是默认行为,也是json和decode的工作方式。但是,有一种方法可以“欺骗”惰性,并告诉编译器立即执行代码并评估值。这就是json'和decode'所做的事情。
其中的权衡是显而易见的:decode在您实际上没有对值执行任何操作的情况下节省计算时间,而decode'则节省了“记住”调用信息(“thunk”)的必要性,代价是在原地执行所有内容。

还有decodeStrictdecodeStrict'函数,它们与decodedecode'不同。事情并不那么简单。不清楚thunk中究竟包含什么以及它如何影响性能。 - Shersh
2
是的,我已经意识到我误解了你的问题。起初看起来你只是不知道严格和惰性之间的区别,但现在在阅读了你的一些评论后,我明白你正在寻找一个更深入的答案。 - Fyodor Soikin
答案是正确的。decodeStrict 变量是用于严格的 ByteString - zaquest
1
@zaquest 虽然这个答案在技术上是正确的,但它并不是 OP 寻找的答案。但还是谢谢你的点赞 :-) - Fyodor Soikin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接