我看过很多会谈/阅读博客文章,你应该在data
中有严格的字段,以避免各种性能问题,例如:
data Person = Person
{ personName :: !Text
, personBirthday :: !UTCTime
}
这对我来说很有意义。由于对该数据的函数操作是惰性的,因此不会牺牲可组合性。
然而,如果我添加Maybe
字段:
data Person = Person
{ personName :: !Text
, personBirthday :: !UTCTime
, personAddress :: !(Maybe Address)
}
我将懒惰引入数据结构,毕竟Maybe
是一个控制结构。不能将Just
构造函数隐藏在未评估的thunk之后?
但是,strict Maybe
或strict
中有strict-base-types
。但是根据反向依赖关系(strict,strict-base-types),它们并没有被广泛使用。
所以问题:为什么在非控制数据定义中应该或不应该使用严格Maybe
?
答案 0 :(得分:2)
使用严格的Either / Maybe / Tuple类型的原因:
如果您对代码进行了分析并注意到空间泄漏,则可能是堵塞泄漏的方法
对于高性能代码,严格的数据类型被广泛认为是有用的,even by the latest GHC 8.0 language extensions
其他人也在这样做(那些严格的软件包可能不受欢迎,但它们存在是有原因的 - 你也可以说你需要那些严格软件包的那种应用程序可能不会上传到Hackage)
原因不是:
不在Prelude中,所以它是一个额外的包
您可能不会编写高性能代码
你的节目可能不会因为你向内推一个级别而变得更快
如果您正在编写高性能代码,可以force evaluation on the thunk inside the Maybe
manually anyway
总的来说,我并不认为这样的教条可以这样或那样。这只是为了方便。
答案 1 :(得分:1)
懒惰和严格都有助于提高绩效;让我们看看懒惰是多么的可能:
显然,如果列表是严格的,sum $ take 10 $ [1..]
需要无限的时间和无限的内存,但如果列表是惰性的,那么有限的时间和常量的内存。
功能数据结构通常不承认不错的摊销界限。什么是"摊销"界?当您在O( n )其他完全无关的步骤之后支付O( f ( n ))的成本时,我们可以富有想象力地重新概念化这是为每个步骤支付O( f ( n )/ n ):所以如果你添加,比如说, n 列表中的元素,然后在 n log n 时间对其进行一次排序,然后您可以重新定义为每次添加日志 n 时间。 (如果你使用自平衡二进制搜索树而不是列表来支持它,它可以做到这一点,但我们可以说即使列表成本是log n 摊销。)
将此与函数式编程相结合的问题在于,当我给你一个新的数据结构时,旧的未被修改,这是一个普遍的承诺,所以作为一个点一般理论,如果转换某些X需要花费很多,那么就存在一种有效的使用模式,它花费 n 来像以前一样构建X,但是然后使用它 m 次以不同的方式(因为它没有被修改!)每次都会产生O( f ( n ))成本:所以现在当你试图分摊你时得到O( m f ( n )/ n ),如果 m 比如说,与 n 成比例,你已经从整个结构产生一次这个成本,而且每增加一个数据结构就会产生一次。你不能说"哦,那不是我的使用案例"当您创建数据结构时:即使它不是您的,也可能可能是其他人,最终发言。
Okasaki指出(在his thesis (PDF)中)懒惰(带有记忆)实际上完全我们需要缩小这个差距:假设 X 进行了后处理-version存储为一个惰性值,然后每个对变换X的调用将达到相同的延迟值并产生相同的memoized答案。所以:如果你能巧妙地将这些东西移到thunk中,那么Haskell不会重新计算thunk的事实可以用来制作memoization参数。
另一个例子,Haskell中的++
是O(1)操作;使用严格列表将大小 n 的列表附加到大小 m 列表的末尾,需要预先进行O( m )内存分配,因为前面的名单需要完全重建;流连接将此转换为O( m )条件操作(幸运的是,这对于处理器中的分支预测器来说非常好!)并且在每次读取列表时分配此成本。
如果你不是使用一堆数据,懒惰有很大的潜力,但你不知道你使用的是哪些东西。举一个简单的例子,如果你不得不反复反转一些在某些有界区间难以预测的昂贵的单调函数,你可能没有一个闭合形式的逆函数或函数或其派生的快速表达式(以便使用Newton-Raphson)。相反,您可以构建一个恒定深度的大型二进制搜索树,其节点使用 f ( x )注释,其叶子代表 x ;然后通过计算 f ( x )和二进制搜索来反转 f 以获取某些输入 x X 。然后每个查询都会自动记忆,因此搜索其他查询附近的值会因为相同的缓存而获得渐近的加速(以不断增加的内存为代价)。
你想要删除懒惰的真实情况是递归数据结构,即使这样,这只适用于你知道你希望整个数据结构在内存中可用(即你&# 39;重新使用所有这些)。此类数据结构通常是 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:Morrowind是如何工作的,但它们包含了一套卷轴作为一个插科打g,如果你能在着陆中幸存下来,它可以让你跳到游戏世界的中途 - 而且你这样做的速度意味着你会比系统加载它们更快地飞过这些区域,所以它会不断给你3秒的空中飞行,然后停下来说2" Loading .. 。",一遍又一遍,当你以这种方式越过游戏世界。没有什么能真正阻止这个问题。)
所以:我们已经了解到某个典型的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, ...}
以便可以将此描述符的多次重复简化为一种通用格式。这对于组织代码来说实际上非常好。