我的重写折叠功能是否已优化?

时间:2012-06-21 18:57:40

标签: haskell tail-call-optimization

我刚刚在2天前创建了Haskell,所以我还不确定如何优化我的代码。

作为练习,我重写了foldlfoldr(我将在此处foldl,但foldr是相同的,将last替换为{{ 1}}和headinit)。

代码是:

tail

我唯一担心的是我怀疑Haskell不能在这里应用尾调用优化,因为递归调用不是在函数结束时进行的。

如何优化尾调用? Haskell的内置实现module Main where myFoldl :: ( a -> ( b -> a ) ) -> a -> ( [b] -> a ) myFoldl func = ( \x -> (\theList -> if (length theList == 0) then x else myFoldl func (func x (last theList) ) (init theList) ) ) 是否与我的实现不同?

2 个答案:

答案 0 :(得分:26)

您在代码示例中使用括号并强调尾部递归表明您将从Lisp或Scheme转到Haskell。如果你是从像Scheme这样急切的语言来到Haskell,请注意:尾部调用几乎不像Haskell那样可以预测性能,因为它们是一种急切的语言。您可以使用由于懒惰而在线性空间中执行的尾递归函数,您可以使用由于懒惰而在常量空间中执行的非尾递归函数。 (已经困惑了?)

您的定义中的第一个缺陷是使用length theList == 0。这会强制评估列表的整个脊柱,并且是O(n)时间。最好使用模式匹配,就像Haskell中这个天真的foldl定义一样:

foldl :: (b -> a -> b) -> b -> [a] -> b
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
然而,这在Haskell中表现得非常糟糕,因为在f z x的调用者要求结果之前我们实际上并不计算foldl部分;所以这个foldl在每个列表元素的内存中累积了未评估的thunk,并且没有从尾递归中获益。事实上,尽管是尾递归,但这个天真的foldl长列表可能会导致堆栈溢出! (Data.List module有一个foldl'函数,没有这个问题。)

与此相反,许多Haskell非尾递归函数表现得非常好。例如,根据伴随的find的非尾递归定义,取foldr的这个定义:

find :: (a -> Boolean) -> [a] -> Maybe a
find pred xs = foldr find' Nothing xs
    where find' elem rest = if pred elem then Just elem else rest

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

由于懒惰,这实际上在线性时间和恒定空间中执行。为什么?

  1. 找到满足谓词的元素后,无需遍历列表的其余部分来计算rest的值。
  2. 一旦你看到一个元素并确定它不匹配,就没有必要保留关于该元素的任何数据。
  3. 我现在要传授的教训是:不要将你渴望的语言中的表现假设引入Haskell。你只有两天了;首先要集中精力理解语言的语法和语义,而不要扭曲自己编写优化版本的函数。你最初会不时遇到foldl式的堆栈溢出,但你会及时掌握它。


    编辑[9/5/2012]:更简单的演示,即懒惰find在恒定空间中运行,尽管不是尾递归。首先,简化定义:

    foldr :: (a -> b -> b) -> b -> [a] -> b
    foldr f z [] = z
    foldr f z (x:xs) = f x (foldr f z xs)
    
    find :: (a -> Bool) -> [a] -> Maybe a
    find p xs = let step x rest = if p x then Just x else rest
                in foldr step Nothing xs
    

    现在,使用等式推理(即,使用等于等于,基于上面的定义),并以惰性顺序(最外面的第一个)进行评估,让我们计算find (==400) [1..]

    find (==400) [1..]
        -- Definition of `find`:
        => let step x rest = if x == 400 then Just x else rest
           in foldr step Nothing [1..]
    
        -- `[x, y, ...]` is the same as `x:[y, ...]`:
        => let step x rest = if x == 400 then Just x else rest
           in foldr step Nothing (1:[2..])
    
        -- Using the second equation in the definition of `foldr`:
        => let step x rest = if x == 400 then Just x else rest
           in step 1 (foldr step Nothing [2..])
    
        -- Applying `step`:
        => let step x rest = if x == 400 then Just x else rest
           in if 1 == 400 then Just 1 else foldr step Nothing [2..]
    
        -- `1 == 400` is `False`
        => let step x rest = if x == 400 then Just x else rest
           in if False then Just 1 else foldr step Nothing [2..]
    
        -- `if False then a else b` is the same as `b`
        => let step x rest = if x == 400 then Just x else rest
           in foldr step Nothing [2..]
    
        -- Repeat the same reasoning steps as above 
        => let step x rest = if x == 400 then Just x else rest
           in foldr step Nothing (2:[3..])
        => let step x rest = if x == 400 then Just x else rest
           in step 2 (foldr step Nothing [3..])
        => let step x rest = if x == 400 then Just x else rest
           in if 2 == 400 then Just 2 else foldr step Nothing [3..]
        => let step x rest = if x == 400 then Just x else rest
           in if False then Just 2 else foldr step Nothing [3..]
        => let step x rest = if x == 400 then Just x else rest
           in foldr step Nothing [3..]
            .
            .
            .
    
        => let step x rest = if x == 400 then Just x else rest
           in foldr step Nothing [400..]
        => let step x rest = if x == 400 then Just x else rest
           in foldr step Nothing (400:[401..])
        => let step x rest = if x == 400 then Just x else rest
           in step 400 (foldr step Nothing [401..])
        => let step x rest = if x == 400 then Just x else rest
           in if 400 == 400 then Just 400 else foldr step Nothing [401..]
        => let step x rest = if x == 400 then Just x else rest
           in if True then Just 400 else foldr step Nothing [401..]
    
        -- `if True then a else b` is the same as `a`
        => let step x rest = if x == 400 then Just x else rest
           in Just 400
    
        -- We can eliminate the `let ... in ...` here:
        => Just 400
    

    请注意,当我们继续执行列表时,连续评估步骤中的表达式不会变得越来越复杂或越来越长;步骤 n 中表达式的长度或深度与 n 不成比例,它基本上是固定的。这实际上说明了find (==400) [1..]如何在恒定空间中懒惰地执行。

答案 1 :(得分:14)

惯用的Haskell看起来与此截然不同,避免使用if-then-else,嵌套的lambdas,括号和解构函数(head,tail)。相反,你会写它像:

foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f z0 xs0 = go z0 xs0
   where
       go z []     = z
       go z (x:xs) = go (f z x) xs

依赖于模式匹配,where子句,尾递归,保护声明。