【问题标题】:Is my rewritten foldl function optimised?我重写的 foldl 函数优化了吗?
【发布时间】:2012-06-21 18:57:39
【问题描述】:

我 2 天前刚开始使用 Haskell,所以我还不确定如何优化我的代码。

作为练习,我重写了foldlfoldr(我将在此处给出foldl,但foldr 是相同的,将last 替换为headinittail) .

代码是:

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)
        ) )

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

如何优化这个尾调用? Haskell 的 foldl 的内置实现与我的实现方式不同吗?

【问题讨论】:

  • 从不使用 if length list == 0 检查空列表。为此使用if null list。如果列表很长甚至无限,那么询问长度是一个非常糟糕的主意。
  • 您也应该谨慎使用init。你知道,它是 O(n)。
  • Data.List/Prelude 定义在这里haskell.org/ghc/docs/latest/html/libraries/base/src/…。请注意重要的不同,并且几乎总是优越的 foldl',它在累加器中是严格的,这里:haskell.org/ghc/docs/latest/html/libraries/base/src/…
  • “我两天前刚开始使用 Haskell,所以我还不确定如何优化我的代码。”现在 that 就是我所说的“过早优化”。在担心优化之前,花点时间学习 Haskell 和纯函数式编程的基本概念。
  • @yatima2975 对此我有点失望。一方面,它修复了一个糟糕的低效率问题(如果使用 GHC 和优化进行编译),另一方面,它似乎消除了修复错误代码的必要性,并且当使用没有重写规则的不同编译器时,它再次爆炸。

标签: haskell tail-call-optimization


【解决方案1】:

您在代码示例中使用括号以及对尾递归的强调表明您是从 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 中表现得非常糟糕,因为在 foldl 的调用者要求结果之前,我们实际上并不计算 f z x 部分;所以这个foldl 会为每个列表元素在内存中累积未评估的thunk,并且不会从尾递归中获得任何好处。事实上,尽管是尾递归的,这个幼稚的foldl 在一个长列表上可能会导致堆栈溢出! (Data.List module 有一个foldl' 函数没有这个问题。)

与此相反,许多 Haskell 非尾递归函数执行得非常好。例如,根据 foldr 的随附非尾递归定义,采用 find 的定义:

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. 一旦您查看某个元素并确定它不匹配,则无需保留有关该元素的任何数据。

我现在要传授的教训是:不要将您对热切语言的性能假设引入 Haskell。你才两天;首先专注于理解语言的语法和语义,不要强迫自己编写优化版本的函数。一开始你会时不时遇到foldl风格的堆栈溢出,但你会及时掌握的。


编辑 [2012 年 9 月 5 日]: 更简单的演示惰性 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

现在,使用等式推理(即,根据上面的定义,用 equals 代替 equals),并以惰性顺序(最外层优先)进行计算,让我们计算 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..] 是如何在恒定空间中延迟执行的。

【讨论】:

    【解决方案2】:

    惯用的 Haskell 看起来与此非常不同,它避开了 if-then-else、嵌套 lambda、括号和解构函数(头、尾)。相反,你可以这样写:

    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 子句、尾递归、受保护的声明。

    【讨论】:

    • 当然你也可以免积分,把foldl f z0 xs0 = go z0 xs0改成foldl f = go
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-06-05
    • 2015-12-22
    • 2021-12-06
    • 2015-05-09
    • 2019-08-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多