这两者之间的区别微妙。虽然有差异,但有点复杂。我们可以从类型入手。
Value
类型很重要的一点是,aeson 提供的该类型自版本 0.4.0.0 以来就一直是 strict 的。这意味着在
Value
的构造函数和其内部表示之间不能有任何 thunks。这立即意味着当一个
Value
被评估为 WHNF 时,
Bool
(当然还有
Null
)必须被完全评估。
接下来,让我们考虑
String
和
Number
。
String
构造函数包含一个类型为
strict 的
Text
值,因此也不能有任何懒惰。同样,
Number
构造函数包含一个
Scientific
值,该值由两个严格的值内部表示。当一个
Value
被评估为 WHNF 时,
String
和
Number
都必须被完全评估。
我们现在可以关注 JSON 提供的唯一非平凡数据类型
Object
和
Array
。它们更有趣。在 aeson 中,
Object
由一个
惰性 的
HashMap
表示。惰性的
HashMap
只评估其键到 WHNF,而不是其值,因此这些值可能仍然是未评估的thunks。同样,
Array
是
Vector
,它们的值也不是严格的。这两种类型的
Value
都可以包含 thunks。
有了这个想法,我们知道,一旦我们有了一个
Value
,
decode
和
decode'
唯一可能不同的地方就在于对象和数组的生成。
观察差异
下一步,我们可以尝试在 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'
现在我们有两个绑定变量,
parsed
和
parsed'
,其中一个使用
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解码,否则不用担心它们之间的选择。在任何情况下,您都应该进行基准测试。在
decode
和
decode'
之间进行选择是一种非常具体的手动优化,如果没有基准测试,我不会非常自信地认为其中任何一个可以改善程序的运行时特性。
aeson
的源代码来看,唯一的区别似乎是字符串和数字是否会被完全强制(从而分配可能昂贵的数字或字符串数据结构)。话虽如此,我还没有能够触发这种行为... - Alecvalue
和value'
的意图是为了严格版本能够急切地构建任何嵌套的对象/数组。实际上,我不确定懒惰版本是否避免了很多工作,因为解析器必须检查是否有任何形成良好的对象/数组,然后才能移动到下一个属性,但我认为在懒惰版本中避免/推迟了在objectValues
/arrayValues
中构建HashMap
/Vector
的复制/处理。 - ryachza