Аккумуляторы в 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 ответ, вероятно, "нет".