Как работает хвостовая рекурсия Haskell?

Я написал этот фрагмент кода и предполагаю, что 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]

person Hynek -Pichi- Vychodil    schedule 05.01.2009    source источник
comment
Сразу хочу отметить - это очень хороший вопрос. Ленивое вычисление имеет интересные побочные эффекты, которые могут быть не сразу очевидны для всех программистов.   -  person jrockway    schedule 10.03.2009
comment
Да, работая с Haskell и другими нечистыми функциональными языками, вы понимаете, что глупые уловки, такие как переписывание для хвостовой рекурсии, часто не нужны или вредны, и вместо этого вам следует сосредоточить свои усилия на том, что действительно нужно оценить.   -  person ephemient    schedule 20.06.2009


Ответы (6)


Помните, что 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]
person eelco    schedule 05.01.2009

Похоже, лень заставляет 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.

person mattiast    schedule 05.01.2009
comment
Хех, я нашел это, он заставляет l оценивать, поэтому в следующем преобразователе рекурсии (l + 1) оценивается до применения следующего len. Это не так-то просто читать и понимать. - person Hynek -Pichi- Vychodil; 05.01.2009
comment
Фактически эта страница ссылается на страницу, которая точно отвечает на вопрос: haskell.org/haskellwiki/Performance/Accumulating_parameter < / а>. - person slim; 12.01.2009

Фолдл несет ту же проблему; он создает преобразователь. Вы можете использовать foldl 'из Data.List, чтобы избежать этой проблемы:

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

Единственная разница между foldl и foldl '- это строгое накопление, поэтому foldl' решает проблему так же, как seq и $! примеры выше. (const.succ) здесь работает так же, как (\ a b -> a + 1), хотя succ имеет менее строгий тип.

person Community    schedule 13.01.2009

Самое простое решение вашей проблемы - включить оптимизацию.

У меня есть ваш исходный код в файле с именем tail.hs.

jmg$ ghc --make tail.hs -fforce-recomp
[1 of 1] Compiling Main             ( tail.hs, tail.o )
Linking tail ...
jmg$ ./tail 
Stack space overflow: current size 8388608 bytes.
Use `+RTS -Ksize -RTS' to increase it.
girard:haskell jmg$ ghc -O --make tail.hs -fforce-recomp
[1 of 1] Compiling Main             ( tail.hs, tail.o )
Linking tail ...
jmg$ ./tail 
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] -> Integer

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

main = print $ myLength [1..10000000]

jmg@girard:/tmp$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 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] Compiling Main             ( length.hs, length.o )
Linking length ...
jmg@girard:/tmp$ time ./length 
Stack space overflow: current size 8388608 bytes.
Use `+RTS -Ksize -RTS' to increase it.

real    0m1.359s
user    0m1.140s
sys 0m0.210s
jmg@girard:/tmp$ ghc -O --make length.hs -fforce-recomp
[1 of 1] Compiling Main             ( length.hs, length.o )
Linking length ...
jmg@girard:/tmp$ time ./length 
10000000

real    0m0.268s
user    0m0.260s
sys 0m0.000s
jmg@girard:/tmp$ 

Итак, если вам интересно, мы можем продолжить выяснять, в чем причина того, что это не удается. Думаю, штаб-квартира GHC воспримет это как ошибку, если такая прямая рекурсия по спискам не будет оптимизирована в эффективный цикл в этом случае.

person jmg    schedule 15.01.2011
comment
Он не работает с кодом из моего вопроса с версией 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 ', чтобы увеличить его. - person Hynek -Pichi- Vychodil; 19.01.2011

Просто чтобы вы знали, есть гораздо более простой способ написать эту функцию:

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

Алекс

person Alex Fort    schedule 06.01.2009
comment
myLength = foldl (+) 0 тоже то же самое, хотя для его доказательства требуются более сложные рассуждения. Оптимизатор сделает его эффективным, поэтому нет необходимости явно выполнять хвостовую рекурсию. - person luqui; 06.01.2009
comment
Вы не правы: * Main ›foldl (+) 0 [1..1000000] *** Исключение: переполнение стека - person Hynek -Pichi- Vychodil; 06.01.2009
comment
И неправильный результат, вместо этого вы суммируете список * Main ›foldl (+) 0 [1..10] =› 55 - person Hynek -Pichi- Vychodil; 06.01.2009
comment
Это можно исправить с помощью foldl ', которая является строгой версией foldl. - person mattiast; 01.02.2009

eelco.lempsink.nl отвечает на ваш вопрос. Вот демонстрация ответа Янна:

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>
person Michael Steele    schedule 26.02.2009