不是“严格就是快,懒就是慢”那么简单。
懒惰和严格都有助于提高性能;让我们看看懒惰是怎样的:
显然sum $ take 10 $ [1..] 如果列表是严格的,则需要无限的时间和无限的内存,但如果列表是惰性的,则需要有限的时间和恒定的内存。
-
函数式数据结构通常不承认良好的摊销界限。什么是“摊销”界限?当您仅在 O(n) 其他完全不相关的步骤之后支付 O(f(n)) 的成本时,我们可以富有想象力地重新概念化这是为每个步骤支付 O(f(n)/n) :所以如果你添加,比如说,n 元素到一个列表中,然后在 n log n 时间内对其进行一次排序,然后您可以重新概念化为每次添加都需要 log n时间。 (如果您使用自平衡二叉搜索树而不是列表来支持它,它会这样做,但我们可以说 即使使用列表,成本也是 log n 摊销.)
将此与函数式编程结合起来的问题在于,当我给你一个新的数据结构时,旧的数据结构不会被修改,所以作为一般理论的观点,如果转换某个 X 的成本很高,那么存在一个有效的使用模式,它像以前一样花费 n 次努力来构建 X,然后以不同的方式使用它 m 次(因为它没有被修改!)每次都会产生 O(f(n)) 成本:所以现在当你尝试摊销时,你只会得到 O(m f(n)/n),如果 m 是,比如说,缩放与 n 成正比,您已经从为整个结构产生一次此成本转变为每次添加到数据结构时基本上产生一次。当您创建数据结构时,您不能说“哦,那不是我的用例”:即使它不是你的,它也可能是别人的, 最后说一下。
Okasaki 指出(在 his thesis (PDF) 中)懒惰(带有记忆)实际上 正是我们需要弥合这个差距:假设 X 有其后处理版本存储为惰性值,然后对变换 X 的每次调用都将达到相同的惰性值并产生相同的记忆答案。所以:如果你能巧妙地将这些东西移动到 thunk 中,那么 Haskell 不会重新计算 thunk 的事实可以用来制作记忆参数。
-
再举一个例子,Haskell 中的++ 是一个 O(1) 操作;使用严格列表将大小为 n 的列表附加到大小为 m 的列表的末尾需要预先分配 O(m) 内存,因为前面的名单需要完全重建;流连接将其转换为 O(m) 条件操作(幸运的是,它与处理器中的分支预测器配合得非常好!)并将此成本分配给列表的每次读取。
李>
如果你不使用一堆数据,但你不知道你在使用哪些东西,那么懒惰就有很大的潜力。举一个简单的例子,如果你必须反复反转一些难以在某个有界区间上预测的昂贵单调函数,你可能没有一个封闭形式的逆或函数或其导数的快速表达式(以便使用Newton-Raphson)。相反,您可以构建一个恒定深度的大二叉搜索树,其节点用 f(x) 注释,其叶子代表 x;然后你通过计算 f(x) 和二进制搜索 来为某些输入 x 反转 f x。然后每个查询都会被自动记忆,因此搜索靠近其他查询的值会由于相同的缓存而获得渐近加速(以不断增加的内存为代价)。
那么,严格在什么时候有帮助?
您想要消除惰性的真正情况是递归数据结构,即使这样,这也仅适用于您知道您希望整个数据结构在内存中可用(即您重新使用所有它)。这样的数据结构通常是spine-strict:例如一个包含实际 值 的 thunk 的列表,但指向其他列表节点的指针都是 100% 严格的。
当这些条件都为真时,将惰性放在每个节点上没有真正意义,因为它提供了额外的 O(n) 成本来评估所有这些 thunk 并且可能会崩溃如果您不使用严格性注释来降低调用堆栈,则将调用堆栈提高到递归限制。如果您对此不是 100% 清楚,我见过的关于这种情况发生的最佳解释是 ones like this, justifying the need for foldl' in cases where both foldr and foldl overflow the call stack for different reasons. 这些解释通常非常实用。
严格也可以预先设置大量成本,例如当你想制作游戏时:如果你懒惰地生成游戏世界,那么当你走进一个全新的区域时,你可能会注意到“缓冲”;但是如果你可以严格提前生成这些东西,你必须付出早期的成本,但你会得到后期的收益。当他们点击“加载游戏”按钮时,人们不介意等待,当它打破沉浸在某个故事中时,他们真的很讨厌等待。 (实际上并行懒惰是真正的理想选择:您希望能够在需要之前在后台强制执行 thunk,同时动作更轻一些,以便在您想要的时候获得结果。甚至然后,虽然——我的意思是,这就是 TES3:晨风的工作方式,但它们包含一组卷轴作为堵嘴,如果你能在着陆中幸存下来,它们可以让你在游戏世界中跳跃一半——以及你想要的速度这样做意味着你飞过这些区域的速度比系统加载它们的速度要快,所以它会不断地给你 3 秒的空中飞行时间,然后停下来说“加载中……”,一遍又一遍,因为你就这样穿越了游戏世界。没有什么能真正阻止这个问题。)
当我需要修复它时,我该如何修复它?
所以:我们了解到,某个地方的典型Maybe 不会为您的应用程序带来可观的成本。这就是为什么没人在乎。
当我们创建一个递归数据结构时,例如替代列表类型data NonNullList x = NNL x !(Maybe (NonNullList x)),它必须总是至少有一个元素怎么办?在这种情况下,递归存在于 Maybe中,我们如何解决这个问题?
是的,您可以使用严格的 Maybe。但是,您也可以内联结构以使其严格。在这种情况下,你会写:
data NonNullList x = End x | Continue x !(NonNullList x)
如果您的数据结构中有太多重复信息(也许我们在数据结构中存储了很多元数据)和太多Maybe (MyDataStructure x) 调用,那么我们最终可能必须有一个data MyDataStructureDescriptor = MDSD { property1 :: !String, property2 :: !Int, ...} 以便该描述符的多次重复可以简化为一种通用格式。这实际上也可以很好地组织您的代码。