gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-02 03:28:02 28 4
gpt4 key购买 nike

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

{-# 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 ,指向内联的方向作为这种辅助局部函数的原因。尽管如此,zipWith标有编译指示 NOINLINE [1] zipWith ,其中,最多 GHC's user guide: 7.13.5.5. Phase control ,意味着在第一阶段之前内联,并且可能在之后内联(无论这意味着什么)。在链接的问题中,PO 指的是 foldr这是使用相同的技巧但没有任何 PRAGMA 来实现的。我认为作者正在避免内联,但我对此了解甚少。

编辑2

添加了链接到source zipWith 的定义

最佳答案

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

您的函数版本使用递归调用zipWith f xs ys。因为 fzipWith 的参数,所以必须重复传递它。这意味着,从 zipWith 来看,f 从一级递归到下一级递归都不会发生变化,这一点并不明显。

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

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

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

关于haskell - 为什么某些 Prelude 函数是根据本地定义的函数定义的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58818027/

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com