为什么某些 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
。因为 f
是 zipWith
的参数,它必须重复传递。这意味着从 zipWith
中看不出 f
从一个递归级别到下一个递归级别从未改变。
Prelude
版本使用递归调用 go xs ys
,这会立即发出信号表明每次对 go
的递归调用都会使用相同的 f
。能够立即注意到这样的不变量有助于我们对代码进行逻辑推理。
编辑:我以前认为内联在这里不相关,但 Carl 和 Daniel Wagner 都指出 thunk f
只需要被评估一次并在 go
的定义中替换,而不是像在 zipWith
.
的定义中那样对每次递归重新评估
你还提到了loop-fusion,就是把相邻的遍历函数合并成一个遍历的过程。由于这已经是单次遍历,这里也不适用。
我正在查看一些前奏函数来教同事递归,我发现有些函数的编写方式 很奇怪 。示例:
{-# 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
正如 Willem Van Onsem 在他的评论中提到的,这意味着 f
不需要在每个递归调用中传递。
您的函数版本使用递归调用 zipWith f xs ys
。因为 f
是 zipWith
的参数,它必须重复传递。这意味着从 zipWith
中看不出 f
从一个递归级别到下一个递归级别从未改变。
Prelude
版本使用递归调用 go xs ys
,这会立即发出信号表明每次对 go
的递归调用都会使用相同的 f
。能够立即注意到这样的不变量有助于我们对代码进行逻辑推理。
编辑:我以前认为内联在这里不相关,但 Carl 和 Daniel Wagner 都指出 thunk f
只需要被评估一次并在 go
的定义中替换,而不是像在 zipWith
.
你还提到了loop-fusion,就是把相邻的遍历函数合并成一个遍历的过程。由于这已经是单次遍历,这里也不适用。