为什么某些 Prelude 函数是根据本地定义的函数定义的?

Why are some Prelude functions defined in terms of a locally defined function?

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

{-# 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

我想这是由于某些 内联/循环融合/惰性/任何特征 允许 GHC 更好地优化,但这是重点Haskell 对我来说变得很模糊。任何人都可以(尽可能简单地)解释这样一个函数定义的原因,如果有的话。

编辑

许多评论,@AndrewRay 的回答和在这个 question, point in the direction of inlining as the reason for such an auxiliar local function. Nevertheless, zipWith is marked with the pragma NOINLINE [1] zipWith, which, up to GHC's user guide: 7.13.5.5. Phase control 中,意味着 在第一阶段之前内联,并且可能在之后内联(不管这意味着什么)。在 linked 问题中,PO 指的是 foldr,它是使用相同的技巧实现的,但没有任何 PRAGMA。我认为作者正在避免内联,但我对此知之甚少。

编辑 2

zipWith

source 定义中添加了 link

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

您的函数版本使用递归调用 zipWith f xs ys。因为 fzipWith 的参数,它必须重复传递。这意味着从 zipWith 中看不出 f 从一个递归级别到下一个递归级别从未改变。

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

编辑:我以前认为内联在这里不相关,但 Carl 和 Daniel Wagner 都指出 thunk f 只需要被评估一次并在 go 的定义中替换,而不是像在 zipWith.

的定义中那样对每次递归重新评估

你还提到了loop-fusion,就是把相邻的遍历函数合并成一个遍历的过程。由于这已经是单次遍历,这里也不适用。