为什么某些Prelude函数是用局部定义的函数定义的?

lsm*_*mor 9 haskell ghc

我正在看一些前奏函数,教给一个同事关于递归的知识,但我发现其中一些是以怪异的方式编写的。例:

{-# 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
Run Code Online (Sandbox Code Playgroud)

为什么将其写为对之后定义的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
Run Code Online (Sandbox Code Playgroud)

我猜这是由于一些内联/循环融合/惰性/可以GHC优化更好的功能,但这就是Haskell对我来说晦涩难懂的地方。任何人都可以(尽可能容易地)解释这种函数定义的原因。

编辑

@AndrewRay的答案以及此问题中的许多评论都指向内联的方向,这是实现此类辅助局部功能的原因。尽管如此,仍以zipWith pragma标记NOINLINE [1] zipWith,根据GHC的用户指南:7.13.5.5。阶段控制,意味着不要在第一阶段之前内联,也可以在之后阶段内联(无论这意味着什么)。在链接的问题中,PO指的foldr是使用相同的技巧实现的,但没有任何PRAGMA。我认为作者在避免内联,但是我对此的了解很少。

编辑2

添加了指向的定义的链接zipWith

And*_*Ray 5

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

您的函数版本使用递归调用zipWith f xs ys。因为f是 的参数zipWith,所以必须反复通过。这意味着,它不是来自看着明显zipWithf永远不会改变的递归的一个水平下。

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

编辑:我以前认为内联在这里不相关,但卡尔和丹尼尔瓦格纳都指出 thunkf只需要评估一次并在 的定义中替换go,而不是像这种情况那样为每次递归重新评估在你的定义中zipWith

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