Аккумуляторы в haskell
в Haskell, если я пишу
fac n = facRec n 1
where facRec 0 acc = acc
facRec n acc = facRec (n-1) (acc*n)
и скомпилировать его с GHC, будет ли результат отличаться от того, если я использовал
fac 0 = 1
fac n = n * fac (n-1)
Я мог бы легко сделать fac n = product [1..n]
и избегайте всего этого, но меня интересует, как попытка хвостовой рекурсии работает на ленивом языке. Я получаю, что я все еще могу получить переполнение стека, потому что thunks строятся, но что-то на самом деле происходит по-другому (с точки зрения результирующей скомпилированной программы), когда я использую аккумулятор, чем когда я просто констатирую наивную рекурсию? Есть ли какая-либо польза от исключения хвостовой рекурсии, кроме улучшенной удобочитаемости? Меняется ли ответ Вообще, если я использую runhaskell
запустить вычисление вместо того, чтобы сначала его компилировать?
3 ответов
хвостовая рекурсия тут имеет смысл в (GHC) Haskell, если ваш аккумулятор строгий. Чтобы продемонстрировать проблему, вот "след" вашего хвоста-рекурсивное определение fac
:
fac 4
~> facRec 4 1
~> facRec 3 (1*4)
~> facRec 2 ((1*4)*3)
~> facRec 1 (((1*4)*3)*2)
~> facRec 0 ((((1*4)*3)*2)*1)
~> (((1*4)*3)*2) * 1
~> ((1*4)*3) * 2
~> (1*4) * 3
~> 1*4
~> 4 * 3
~> 12 * 2
~> 24 * 1
~> 24
уровень отступа соответствует (грубо) уровню стека. Обратите внимание, что аккумулятор оценивается только в самом конце, и это может привести к переполнению стека. Фокус, конечно, в том, чтобы сделать аккумулятор строгим. Теоретически возможно показать, что facRec strict, если он вызывается в строгом контексте, но я не знаю ни одного компилятора, который делает это, ATM. С GHC тут сделать оптимизацию хвостового вызова, хотя, так что facRec
вызовы используют постоянное пространство стека.
по той же причине foldl'
- это, как правило, предпочтительнее foldl
, так как первый является строгим в аккумуляторе.
Что касается вашей второй части,runhaskell
/runghc
это просто обертка над GHCi. Если GHCi найдет скомпилированный код, он будет использовать его, иначе он будет используйте интерпретатор байт-кода, который выполняет несколько оптимизаций, поэтому не ожидайте каких-либо причудливых оптимизаций.
в haskell это помогает только писать вашу программу хвостовым рекурсивным способом, если ваш аккумулятор строг, и вам нужен весь результат.
с runHaskell ghc программа не будет оптимизирована, поэтому не будет анализа строгости, поэтому вы можете переполнить стек; в то время как при компиляции с оптимизациями компилятор может обнаружить, что аккумулятор должен быть строгим и оптимизировать соответственно.
чтобы увидеть, как все происходит иначе (или нет) лучший способ это проверить основной язык генерируется,хороший пост в блоге от Дона Стюарта объясняет вещи . Многие из его сообщений в блоге интересны, если вас интересует производительность, кстати.
Ваш вопрос не полный. Я предполагаю, что вы имеете в виду GHC, и, по крайней мере, без оптимизации ответ "да", потому что рабочая функция (facRec
в первом или fac
во втором) имеет арность 2 по сравнению с одним и сборка будет отражать это. С оптимизациями или с JHC ответ, вероятно, "нет".