【问题标题】:Why are some Prelude functions defined in terms of a locally defined function? [duplicate]为什么某些 Prelude 函数是根据本地定义的函数来定义的? [复制]
【发布时间】:2019-11-12 11:44:13
【问题描述】:

我正在查看一些前奏函数来教同事递归,我发现有些是以一种相当奇怪的方式编写的。示例:

{-# NOINLINE [1] zipWith #-}
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f = go
  where
    go [] _ = []
    go _ [] = []
    go (x:xs) (y:ys) = f x y : go xs ys

为什么它被写成对go 的调用,而go 是在之后定义的? IMO 更自然的定义方式是:

zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith _ [] _ = []
zipWith _ _ [] = [] 
zipWith f (x:xs) (y:ys) = f x y : zipWith f xs ys

我猜这是由于一些 inline / loop-fusion / lazy / whatever-feature 允许 GHC 更好地优化,但这就是 Haskell 对我来说变得非常模糊的地方.任何人都可以解释(尽可能简单)这种函数定义的原因,如果有的话。

编辑

许多 cmets,@AndrewRay 的回答以及在这个 question 中,都指出内联的方向是这种辅助局部函数的原因。尽管如此,zipWith 被标记为 pragma NOINLINE [1] zipWith,直到 GHC's user guide: 7.13.5.5. Phase control,意味着在第一阶段之前内联,并且可能在之后内联(无论这意味着什么)。在链接的问题中,PO 指的是foldr,它是使用相同的技巧实现的,但没有任何 PRAGMA。我认为作者正在避免内联,但我对此的了解很少。

编辑 2

添加了source定义zipWith的链接

【问题讨论】:

  • 避免每次都传递f
  • @WillemVanOnsem 这是一个快速的答案!你能详细说明一下吗?这似乎并不明显,因为 go 实际上已通过,并且 go 在其定义中包含 f ......不是吗?
  • “更自然”在旁观者的眼中。对我来说go 很自然。它压缩了它的两个列表参数。用什么拉链?有了它知道的东西。那个“东西”与压缩的递归机制无关。
  • @n.'pronouns'm。奥卡姆剃刀没有任何主观性。 go 是一个新实体,它没有任何概念——他们甚至没有想出一个真实的名字。所以这是一种精心设计,而且在外延上是无关紧要的——不自然
  • 您甚至没有最易读的go 版本。首先定义go (x:xs) (y:ys),然后其他两个基本情况减少为单个情况go _ _ = []

标签: haskell ghc


【解决方案1】:

正如 Willem Van Onsem 在他的评论中提到的,这意味着 f 不需要在每个递归调用中传递。

您的函数版本使用递归调用zipWith f xs ys。因为fzipWith 的参数,所以它必须重复传递。这意味着从zipWith 来看,f 永远不会从一个递归级别更改为下一个级别,这一点并不明显。

Prelude 版本使用递归调用go xs ys,它立即表示对go 的每个递归调用都使用相同的f。能够立即注意到这样的不变量有助于我们对代码进行逻辑推理。

编辑:我之前认为内联在这里无关紧要,但是 Carl 和 Daniel Wagner 都指出 thunk f 只需要评估一次并替换为go,而不是像您对 zipWith 的定义那样为每次递归重新评估。

您还提到了循环融合,即将相邻的遍历函数合并为单个遍历的过程。由于这已经是一次遍历,因此这里也不适用。

【讨论】:

  • zipWithgo 是自递归的吗?
  • 编译代码的性能是 100%。使顶级定义为非递归允许它被内联,这导致go 被定义在f 在范围内的某个地方,这意味着如果f 已知,它将被内联到递归部分。这减少了对潜在显着加速的间接函数调用的使用。
  • 您说这与性能无关,但实际上是。如果你像 GHC 的 Prelude 那样做一个闭包,你将指针复制到f 的 thunk 一次,然后在你生成的每个列表元素的闭包中查找它一次。如果你不这样做,就像在建议的替代实现中一样,你将指针复制到f 的每个生成的列表元素的 thunk 一次,然后仍然必须在新创建的闭包中查找它。 GHC 足够聪明,可以有时消除后一种成本,因此如今这种技术只有在多个参数为只读时才会更快,但并非总是如此。
  • @Carl,这是一个公平的观点;我不认为 zipWith 本身是内联的。
  • @DanielWagner,我原以为无论如何都会优化掉它,但我会听从你在这方面的专业知识。
猜你喜欢
  • 2011-12-22
  • 1970-01-01
  • 1970-01-01
  • 2013-06-09
  • 1970-01-01
  • 1970-01-01
  • 2013-06-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多