[Haskell] Хвостовая рекурсия
От: Димчанский Литва http://dimchansky.github.io/
Дата: 13.02.12 10:05
Оценка:
До этого игрался с F#, решил поближе познакомиться с Haskell.
Смотрю исходники Haskell и возник вопрос по поводу хвостовой рекурсии.
Например, код для вычисления длины списка.

-- | The 'genericLength' function is an overloaded version of 'length'.  In
-- particular, instead of returning an 'Int', it returns any type which is
-- an instance of 'Num'.  It is, however, less efficient than 'length'.
genericLength           :: (Num i) => [b] -> i
genericLength []        =  0
genericLength (_:l)     =  1 + genericLength l


Если бы я писал на F#, то сделал бы рекурсию с аккумулятором, чтобы на стеке точно ничего не оставалось и рекурсия была хвостовой. В Haskell, смотрю, с этим как-то осбенно не заморачиваются.
Это из-за ленивости языка и его способности преобразовывать такую рекурсию в хвостовую?

Общий вопрос: как писать рекурсии на Haskell, чтобы быть уверенным в том, что он сделает рекурсию хвостовой?

Потому что, если бы я в F# написал:
let rec length = function
    | []         -> 0
    | _ :: tail -> 1 + length tail


То, выполнение кода

length [1..10000000]


привело бы к StackOverflowException.
... << RSDN@Home 1.2.0 alpha 5 rev. 1536>>
Re: [Haskell] Хвостовая рекурсия
От: Klapaucius  
Дата: 13.02.12 12:18
Оценка: 16 (2)
Здравствуйте, Димчанский, Вы писали:

Д>Это из-за ленивости языка и его способности преобразовывать такую рекурсию в хвостовую?


0) Никакого преобразования в хвостовую рекурсию не происходит. Хвостовая рекурсия в ленивом языке далеко не лучший вариант, к которому нужно стремиться. См. тут
Автор: Klapaucius
Дата: 30.11.11
, например.
1) Если в строгом ФЯ все крутится вокруг "как бы нам тут сделать хвостовую рекурсию" в ленивом ФЯ более важная концепция — guarded recursion, и вообще см. тут.
2) "Переполнение стека" в ghc-ном рантайме это переполнение особого участка кучи, ограничение можно установить каким угодно — только бы памяти хватило, существует оно скорее в диагностических целях. В этом ghc-ный рантайм похож скорее на sml/nj чем на F#. Но код, использующий большие объемы "стека", лучше не писать, хотя-бы по той причине, что он обычно тормозной, точнее практически всегда можно написать и побыстрее.

Д>Общий вопрос: как писать рекурсии на Haskell, чтобы быть уверенным в том, что он сделает рекурсию хвостовой?


Рекурсии вообще лучше не писать, а пользоваться стандартными комбинаторами.
... << RSDN@Home 1.2.0 alpha 4 rev. 1476>>
'You may call it "nonsense" if you like, but I'VE heard nonsense, compared with which that would be as sensible as a dictionary!' (c) Lewis Carroll
Re[2]: [Haskell] Хвостовая рекурсия
От: Димчанский Литва http://dimchansky.github.io/
Дата: 13.02.12 16:44
Оценка:
Спасибо за развернутый ответ и ссылки. Прочитал все, но у меня остались все же некоторые неясности.

K>Но код, использующий большие объемы "стека", лучше не писать, хотя-бы по той причине, что он обычно тормозной, точнее практически всегда можно написать и побыстрее.


С точки зрения, что код, использующий большие объемы стека, лучше не писать, можно сделать вывод, что длину списка лучше вычислять, например, так:

lengthAux           :: (Num i) => i -> [b] -> i
lengthAux n [] = n
lengthAux n (_:l) = lengthAux (n+1) l

length'           :: (Num i) => [b] -> i
length' = lengthAux 0


На стек ничего не кладется, код написан так, чтобы не использовать большие объемы "стека".
Чем это хуже?

По последней ссылке есть такие слова:

In many programming languages, calling a function uses stack space, so a function that is tail recursive can build up a large stack of calls to itself, which wastes memory. Since in a tail call, the containing function is about to return, its environment can actually be discarded and the recursive call can be entered without creating a new stack frame. This trick is called tail call elimination or tail call optimisation and allows tail-recursive functions to recurse indefinitely.

In Haskell, the function call model is a little different, function calls might not use a new stack frame, so making a function tail-recursive typically isn't as big a deal—being productive, via guarded recursion, is more usually a concern.


Хочется понять физику. Чем модель вызова фукций в Хаскеле отличается от других языков.

K>Рекурсии вообще лучше не писать, а пользоваться стандартными комбинаторами.


Ну нужно начать что-то писать, чтобы почувствовать, как можно избегать рекурсий. Потому что пока не чувствую, как можно совсем без рекурсий.
... << RSDN@Home 1.2.0 alpha 5 rev. 1536>>
Re: [Haskell] Хвостовая рекурсия
От: Паблик Морозов  
Дата: 13.02.12 16:55
Оценка:
Здравствуйте, Димчанский, Вы писали:

Конечно хлопнется со stack overflow, но ведь Хаскель для настоящих мужыков, готовых защищать дисеры по факториалам:

http://hackage.haskell.org/trac/ghc/ticket/2962

И если ты заглянешь в быдлокод (http://www.haskell.org/ghc/docs/latest/html/libraries/base-4.5.0.0/src/Data-List.html#genericLength), то увидишь там правила перезаписи для Int и Integer, с хвостовой рекурсией и форсированием ленивости (потому что иначе даже хвосторекурсивная функция хлопнется на вычислении санков — Хаскель такой Хаскель).

Приводить к хвосторекурсивной форме и форсировать ленивость в общем случае не совсем корректно, потому что натуральные числа могут быть заданы как data Nat = Zero | Succ Nat и нам может захотеться, чтобы такое число начинало строиться с Zero, ставя каждое следующее число в соответсвие следующему элементу списка.
Re[3]: [Haskell] Хвостовая рекурсия
От: Курилка Россия http://kirya.narod.ru/
Дата: 13.02.12 17:00
Оценка:
Здравствуйте, Димчанский, Вы писали:

Д>Спасибо за развернутый ответ и ссылки. Прочитал все, но у меня остались все же некоторые неясности.


K>>Но код, использующий большие объемы "стека", лучше не писать, хотя-бы по той причине, что он обычно тормозной, точнее практически всегда можно написать и побыстрее.


Д>С точки зрения, что код, использующий большие объемы стека, лучше не писать, можно сделать вывод, что длину списка лучше вычислять, например, так:


Д>
lengthAux           :: (Num i) => i -> [b] -> i
Д>lengthAux n [] = n
Д>lengthAux n (_:l) = lengthAux (n+1) l

Д>length'           :: (Num i) => [b] -> i
Д>length' = lengthAux 0
Д>


Д>На стек ничего не кладется, код написан так, чтобы не использовать большие объемы "стека".

Д>Чем это хуже?

"Ничего" не пойдёт на стек, если сделаем, например, так:

{-# LANGUAGE BangPatterns #-}
lengthAux :: (Num i) => i -> [b] -> i
lengthAux n [] = n
lengthAux !n (_:l) = lengthAux (n+1) l

length' :: (Num i) => [b] -> i
length' = lengthAux 0

Re[4]: [Haskell] Хвостовая рекурсия
От: Димчанский Литва http://dimchansky.github.io/
Дата: 13.02.12 17:03
Оценка:
Здравствуйте, Курилка, Вы писали:

К>

К>{-# LANGUAGE BangPatterns #-}
К>lengthAux :: (Num i) => i -> [b] -> i
К>lengthAux n [] = n
К>lengthAux !n (_:l) = lengthAux (n+1) l

К>length' :: (Num i) => [b] -> i
К>length' = lengthAux 0


А можно для неокрепших бойцов пояснить что такое "LANGUAGE BangPatterns" и что за волшебство делает "!"?
... << RSDN@Home 1.2.0 alpha 5 rev. 1536>>
Re[5]: [Haskell] Хвостовая рекурсия
От: Курилка Россия http://kirya.narod.ru/
Дата: 13.02.12 17:05
Оценка:
Здравствуйте, Димчанский, Вы писали:

Д>Здравствуйте, Курилка, Вы писали:


К>>

К>>{-# LANGUAGE BangPatterns #-}
К>>lengthAux :: (Num i) => i -> [b] -> i
К>>lengthAux n [] = n
К>>lengthAux !n (_:l) = lengthAux (n+1) l

К>>length' :: (Num i) => [b] -> i
К>>length' = lengthAux 0


Д>А можно для неокрепших бойцов пояснить что такое "LANGUAGE BangPatterns" и что за волшебство делает "!"?


борется с санками, чтож ещё
Re[3]: [Haskell] Хвостовая рекурсия
От: Klapaucius  
Дата: 13.02.12 18:34
Оценка:
Здравствуйте, Димчанский, Вы писали:

Д>Прочитал все, но у меня остались все же некоторые неясности.


Это совершенно нормально. Дальше неясностей станет только больше.

Д>С точки зрения, что код, использующий большие объемы стека, лучше не писать, можно сделать вывод, что длину списка лучше вычислять, например, так:

Д>
lengthAux           :: (Num i) => i -> [b] -> i
Д>lengthAux n [] = n
Д>lengthAux n (_:l) = lengthAux (n+1) l

Д>length'           :: (Num i) => [b] -> i
Д>length' = lengthAux 0
Д>

Д>На стек ничего не кладется, код написан так, чтобы не использовать большие объемы "стека".

Дело в том, что n+1 в этом коде ничего и не делает, кроме занимания все больших объемов. Язык-то ленивый. Это, по крайней мере, справедливо для Int, Integer, Double и прочих ходовых инстансов Num.

Д>Хочется понять физику. Чем модель вызова фукций в Хаскеле отличается от других языков.


"Немного отличается" — это сильное преуменьшение. Чтоб понимать "физику" нужно читать это и далее по ссылкам. Но для написания программ на хаскеле это все, к счастью, не обязательно. По крайней мере, не для первого чтения.
... << RSDN@Home 1.2.0 alpha 4 rev. 1476>>
'You may call it "nonsense" if you like, but I'VE heard nonsense, compared with which that would be as sensible as a dictionary!' (c) Lewis Carroll
Re[4]: [Haskell] Хвостовая рекурсия
От: Димчанский Литва http://dimchansky.github.io/
Дата: 13.02.12 19:03
Оценка:
Здравствуйте, Klapaucius, Вы писали:

K>Это совершенно нормально. Дальше неясностей станет только больше.


Это обнадеживает.

K>Дело в том, что n+1 в этом коде ничего и не делает, кроме занимания все больших объемов. Язык-то ленивый.


Вооот, теперь понятно. После энергичного F# совершенно об этом забыл. Если переписать в терминах F#, то там в аккумуляторе будет формироваться длинная цепочка Lazy объектов с каждым инкрементом.

K>"Немного отличается" — это сильное преуменьшение. Чтоб понимать "физику" нужно читать это и далее по ссылкам. Но для написания программ на хаскеле это все, к счастью, не обязательно. По крайней мере, не для первого чтения.


Ну "little different" это их же слова, мопед не мой.
Re[4]: [Haskell] Хвостовая рекурсия
От: Аноним  
Дата: 15.02.12 07:43
Оценка:
Здравствуйте, Курилка, Вы писали:

К>"Ничего" не пойдёт на стек, если сделаем, например, так:


К>

К>{-# LANGUAGE BangPatterns #-}
К>lengthAux !n (_:l) = lengthAux (n+1) l


Форсировать n тут совсем ни к чему. Оно и так будет форсированно плюсом. Так что оба варианта эквивалентны (даже на уровне Core, по крайней мере при компиляции в GHC 7.0.3).
Re[5]: [Haskell] Хвостовая рекурсия
От: Аноним  
Дата: 15.02.12 09:45
Оценка:
Здравствуйте, Аноним, Вы писали:

А>Форсировать n тут совсем ни к чему. Оно и так будет форсированно плюсом.


Ан нет. Глупость конечно же написал. Эквивалентны эти примеры только с -O компиляции (за счет анализатора строгости), а без оного нужно форсировать либо !-ом либо seq-ом.
lengthAux           :: (Num i) => i -> [b] -> i
lengthAux n [] = n
lengthAux n (_:l) = n `seq` lengthAux (n+1) l
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.