【问题标题】:How does Haskell tail recursion work?Haskell 尾递归如何工作?
【发布时间】:2011-01-20 16:18:06
【问题描述】:

我编写了这段 sn-p 代码,我假设 len 是尾递归的,但仍然会发生堆栈溢出。怎么了?

myLength :: [a] -> Integer

myLength xs = len xs 0
    where len [] l = l
          len (x:xs) l = len xs (l+1)

main = print $ myLength [1..10000000]

【问题讨论】:

  • 我只想指出——这是一个非常好的问题。惰性求值有一些有趣的副作用,可能并不是所有程序员都立即发现的。
  • 是的,与其他非纯函数式语言相比,使用 Haskell 工作时,您会意识到重写尾递归等愚蠢的技巧通常是不必要或有害的,您应该将精力集中在真正需要的东西上进行评估。

标签: haskell tail-recursion


【解决方案1】:

记住 Haskell 是懒惰的。在绝对必要之前,您的计算 (l+1) 不会发生。

“简单”的解决方法是使用“$!”强制评估:

myLength :: [a] -> Integer
myLength xs = len xs 0
where len [] l = l
      len (x:xs) l = len xs $! (l+1)

      main = print $ myLength [1..10000000]

【讨论】:

    【解决方案2】:

    似乎懒惰导致len 构建thunk:

    len [1..100000] 0
    -> len [2..100000] (0+1)
    -> len [3..100000] (0+1+1)
    

    等等。每次都必须强制len 减少l

    len (x:xs) l = l `seq` len xs (l+1)
    

    更多信息,请关注http://haskell.org/haskellwiki/Stack_overflow

    【讨论】:

    • 嘿,我找到了,它强制 l 进行评估,所以在下一个递归 thunk (l+1) 中,在下一个 len 应用之前进行评估。它不是那么容易阅读和理解。
    • 事实上,该页面链接到一个准确回答问题的页面:haskell.org/haskellwiki/Performance/Accumulating_parameter
    【解决方案3】:

    foldl 也有同样的问题;它建立了一个thunk。您可以使用 Data.List 中的 foldl' 来避免该问题:

    import Data.List
    myLength = foldl' (const.succ) 0
    

    foldl 和 foldl' 唯一的区别就是严格的累加,所以 foldl' 解决问题的方法和 seq 和 $!上面的例子。 (const.succ) 这里的工作方式与 (\a b -> a+1) 相同,但 succ 的类型限制较少。

    【讨论】:

      【解决方案4】:

      解决问题的最简单方法是开启优化。

      我在一个名为 tail.hs 的文件中有你的源代码。

      jmg$ ghc --make tail.hs -fforce-recomp [1 of 1] 编译 Main (tail.hs, tail.o) 连接尾... jmg$ ./尾 堆栈空间溢出:当前大小 8388608 字节。 使用`+RTS -Ksize -RTS'来增加它。 girard:haskell jmg$ ghc -O --make tail.hs -fforce-recomp [1 of 1] 编译 Main (tail.hs, tail.o) 连接尾... jmg$ ./尾 10000000 jmg$

      @Hynek -Pichi- Vychodil 上面的测试是在 Mac OS X Snow Leopard 64bit 和 GHC 7 和 GHC 6.12.1 上完成的,每个都是 32 位版本。在你投反对票后,我在 Ubuntu Linux 上重复了这个实验,结果如下:

      jmg@girard:/tmp$ cat length.hs myLength :: [a] -> 整数 我的长度 xs = 长度 xs 0 其中 len [] l = l 长度 (x:xs) l = 长度 xs (l+1) 主 = 打印 $ myLength [1..10000000] jmg@girard:/tmp$ ghc --version Glorious Glasgow Haskell 编译系统,版本 6.12.1 jmg@girard:/tmp$ uname -a Linux girard 2.6.35-24-generic #42-Ubuntu SMP Thu Dec 2 02:41:37 UTC 2010 x86_64 GNU/Linux jmg@girard:/tmp$ ghc --make length.hs -fforce-recomp [1 of 1] 编译 Main (length.hs, length.o) 连接长度... jmg@girard:/tmp$ 时间 ./length 堆栈空间溢出:当前大小 8388608 字节。 使用`+RTS -Ksize -RTS'来增加它。 真实0m1.359s 用户 0m1.140s 系统 0m0.210s jmg@girard:/tmp$ ghc -O --make length.hs -fforce-recomp [1 of 1] 编译 Main (length.hs, length.o) 连接长度... jmg@girard:/tmp$ 时间 ./length 10000000 实际0m0.268s 用户 0m0.260s 系统 0m0.000s jmg@girard:/tmp$

      因此,如果您有兴趣,我们可以继续找出导致您失败的原因。我猜,GHC 总部会接受它作为一个错误,如果在这种情况下没有将这种对列表的直接递归优化为一个有效的循环。

      【讨论】:

      • 它的代码来自我的问题,版本为 6.12.1 $ ghc -O --make length.hs -fforce-recomp [1 of 1] Compiling Main ( length.hs, length.o ) Linking length ... hynek@hynek:~/work/haskell$ ./length Stack space overflow: current size 8388608 bytes. Use +RTS -Ksize -RTS' 以增加它。`
      【解决方案5】:

      您知道,有一种更简单的方法来编写此函数:

      myLength xs = foldl step 0 xs where step acc x = acc + 1

      亚历克斯

      【讨论】:

      • myLength = foldl (+) 0 也是相同的,尽管需要更复杂的推理来证明这一点。优化器将使其高效,因此无需显式地尾递归。
      • 你不对:*Main> foldl (+) 0 [1..1000000] *** 异常:堆栈溢出
      • 还有错误的结果,你把列表和 *Main> foldl (+) 0 [1..10] => 55
      • 这可以使用 foldl' 修复,它是 foldl 的严格版本。
      【解决方案6】:

      eelco.lempsink.nl 回答了您提出的问题。这是 Yann 答案的演示:

      module Main
          where
      
      import Data.List
      import System.Environment (getArgs)
      
      main = do
        n <- getArgs >>= readIO.head
        putStrLn $ "Length of an array from 1 to " ++ show n
                     ++ ": " ++ show (myLength [1..n])
      
      myLength :: [a] -> Int
      myLength = foldl' (const . succ) 0
      

      foldl' 每次将 1 加到从 0 开始的累加器时,从左到右遍历列表。

      这是一个运行程序的例子:


      C:\haskell>ghc --make Test.hs -O2 -fforce-recomp
      [1 of 1] Compiling Main             ( Test.hs, Test.o )
      Linking Test.exe ...
      
      C:\haskell>Test.exe 10000000 +RTS -sstderr
      Test.exe 10000000 +RTS -sstderr
      
      Length of an array from 1 to 10000000: 10000000
           401,572,536 bytes allocated in the heap
                18,048 bytes copied during GC
                 2,352 bytes maximum residency (1 sample(s))
                13,764 bytes maximum slop
                     1 MB total memory in use (0 MB lost due to fragmentation)
      
        Generation 0:   765 collections,     0 parallel,  0.00s,  0.00s elapsed
        Generation 1:     1 collections,     0 parallel,  0.00s,  0.00s elapsed
      
        INIT  time    0.00s  (  0.00s elapsed)
        MUT   time    0.27s  (  0.28s elapsed)
        GC    time    0.00s  (  0.00s elapsed)
        EXIT  time    0.00s  (  0.00s elapsed)
        Total time    0.27s  (  0.28s elapsed)
      
        %GC time       0.0%  (0.7% elapsed)
      
        Alloc rate    1,514,219,539 bytes per MUT second
      
        Productivity 100.0% of total user, 93.7% of total elapsed
      
      
      C:\haskell>
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-03-09
        • 2012-10-14
        • 2015-05-09
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多