为什么foldr使用辅助函数?

时间:2014-10-07 18:44:39

标签: haskell fold

在向Haskell新手解释foldr时,规范定义是

foldr            :: (a -> b -> b) -> b -> [a] -> b
foldr _ z []     =  z
foldr f z (x:xs) =  f x (foldr f z xs)

但是在GHC.Base中,foldr被定义为

foldr k z = go
          where
            go []     = z
            go (y:ys) = y `k` go ys

似乎这个定义是对速度的优化,但我不明白为什么使用辅助函数go会使速度更快。源评论(see here)提到内联,但我也不知道这个定义如何改进内联。

4 个答案:

答案 0 :(得分:34)

我可以添加一些关于GHC优化系统的重要细节。

foldr的幼稚定义绕过一个函数。调用函数有一个固有的开销 - 特别是在编译时不知道该函数时。如果在编译时已知函数的内容,那么能够内联函数的定义真的很棒。

有一些技巧可以在GHC中执行内联 - 这就是它们的一个例子。首先,foldr需要内联(我以后会说明原因)。 foldr天真的实现是递归的,所以不能内联。因此,将工作者/包装器转换应用于定义。 worker是递归的,但包装器不是。这允许foldr内联,尽管递归列表结构。

内联foldr时,它也会创建所有本地绑定的副本。它或多或少是一个直接的文本内联(模数重命名,并在desugaring传递之后发生)。这是事情变得有趣的地方。 go是一个本地绑定,优化器可以查看它。它注意到它在本地范围内调用一个函数,它命名为k。 GHC通常会完全删除k变量,并将其替换为表达式k减少到的变量。然后,如果函数应用程序适合内联,则可以在此时内联 - 消除完全调用第一类函数的开销。

让我们看一个简单,具体的例子。此程序将回显一行输入,删除所有尾随'x'个字符:

dropR :: Char -> String -> String
dropR x r = if x == 'x' && null r then "" else x : r

main :: IO ()
main = do
    s <- getLine
    putStrLn $ foldr dropR "" s

首先,优化器将内联foldr的内容并进行简化,从而产生如下代码:

main :: IO ()
main = do
    s <- getLine
    -- I'm changing the where clause to a let expression for the sake of readability
    putStrLn $ let { go [] = ""; go (x:xs) = dropR x (go xs) } in go s

工作包装器转换允许的事情......我将跳过剩下的步骤,但很明显,GHC现在可以内联dropR的定义,消除函数调用开销。这是获得巨大成功的地方。

答案 1 :(得分:15)

GHC无法内联递归函数,所以

foldr            :: (a -> b -> b) -> b -> [a] -> b
foldr _ z []     =  z
foldr f z (x:xs) =  f x (foldr f z xs)

无法内联。但是

foldr k z = go
      where
        go []     = z
        go (y:ys) = y `k` go ys

不是递归函数。它是一个带有局部递归定义的非递归函数!

这意味着,正如@bheklilr写的那样,map (foldr (+) 0) foldr可以内联fz(+)替换为0和{{} 1}}在新的go中,可能会发生很多事情,例如中间值的拆箱。

答案 2 :(得分:14)

正如评论所说:

-- Inline only in the final stage, after the foldr/cons rule has had a chance
-- Also note that we inline it when it has *two* parameters, which are the
-- ones we are keen about specialising!

特别注意&#34;当它有两个参数时,我们会内联它,这是我们热衷于专业化的参数!&#34;

这说的是,当foldr被内联时,它仅针对特定选项fz进行内联,而不是列表的选择折叠起来。我不是专家,但它似乎可以在像

这样的情况下内联它
map (foldr (+) 0) some_list

以便内联在此行中发生,而不是在应用map之后。这使得它在更多情况下更容易优化。所有辅助函数都掩盖了第三个参数,因此{-# INLINE #-}可以做到这一点。

答案 3 :(得分:7)

其他答案中没有提到的一个很小的重要细节是GHC,给出了像

这样的函数定义
f x y z w q = ...
在应用所有参数fxyzw之前,

无法内联q。这意味着使用worker / wrapper转换来公开一组最小的函数参数通常是有利的,这些函数参数必须在内联之前应用。