Существуют ли проблемы, которые невозможно записать с помощью хвостовой рекурсии?

Рекурсия хвоста является важной функцией оптимизации производительности в функциональных языках, поскольку она позволяет рекурсивным вызовам потреблять постоянный стек (а не O (n)).

Существуют ли какие-либо проблемы, которые просто не могут быть записаны в хвосто-рекурсивном стиле или всегда можно преобразовать наивно-рекурсивную функцию в хвосто-рекурсивную?

Если это так, в один прекрасный день функциональные компиляторы и интерпретаторы могут быть достаточно интеллектуальными для автоматического преобразования?

Ответ 1

Да, на самом деле вы можете взять какой-то код и преобразовать каждый вызов функции — и каждый return — в хвостовой вызов. То, что вы закончите, называется стилем продолжения-прохода или CPS.

Например, здесь функция, содержащая два рекурсивных вызова:

(define (count-tree t)
  (if (pair? t)
    (+ (count-tree (car t)) (count-tree (cdr t)))
    1))

И вот как это выглядело бы, если бы вы преобразовали эту функцию в стиль продолжения прохождения:

(define (count-tree-cps t ctn)
  (if (pair? t)
    (count-tree-cps (car t)
                    (lambda (L) (count-tree-cps (cdr t)
                                                (lambda (R) (ctn (+ L R))))))
    (ctn 1)))

Дополнительный аргумент ctn - это процедура, в которой count-tree-cps хвостовые вызовы вместо возврата. (sdcvvc отвечает, что вы не можете делать все в O (1) пространстве, и это правильно, здесь каждое продолжение - это замыкание, которое занимает некоторую память.)

Я не преобразовал вызовы в car или cdr или + в хвостовые вызовы. Это также можно было бы сделать, но я предполагаю, что эти листовые вызовы действительно будут встроены.

Теперь для забавной части. Chicken Scheme фактически выполняет это преобразование во всем компилируемом коде. Процедуры, составленные Куриной, никогда не возвращаются. Там классическая статья, объясняющая, почему Chicken Scheme делает это, написанная в 1994 году до того, как Chicken был реализован: CONS не должна противоречить его аргументам. Часть II: Чейни на MTA

Удивительно, но стиль продолжения прохождения довольно распространен в JavaScript. Вы можете использовать его для выполнения длительных вычислений, избегая всплывающего окна браузера "slow script". И это привлекательно для асинхронных API. jQuery.get (простая оболочка вокруг XMLHttpRequest) явно находится в стиле продолжения прохождения; последний аргумент - это функция.

Ответ 2

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

Полезно знать, что многие функции, которые не являются явно хвосторекурсивными, могут быть преобразованы в хвосто-рекурсивную форму путем добавления накопительных параметров. (Крайняя версия этого преобразования - это переход к стилю продолжения прохождения (CPS), но большинство программистов считают, что результат преобразования CPS трудно читать.)

Вот пример функции, которая является "рекурсивной" (на самом деле она просто повторяется), но не является хвостовой рекурсивной:

factorial n = if n == 0 then 1 else n * factorial (n-1)

В этом случае умножение происходит после рекурсивного вызова. Мы можем создать версию, рекурсивную с хвостом, поставив продукт в накопительный параметр:

factorial n = f n 1
   where f n product = if n == 0 then product else f (n-1) (n * product)

Внутренняя функция f является хвостовой рекурсивной и компилируется в замкнутый цикл.


Я нахожу следующие отличия полезными:

  • В итеративной или рекурсивной программе вы решаете проблему размера n на сначала решая одну подзадачу размера n-1. Вычисление факторной функции попадает в эту категорию, и это можно сделать либо итеративно, либо рекурсивно. (Эта идея обобщает, например, функцию Фибоначчи, где вам нужно как n-1, так и n-2 решить n.)

  • В рекурсивной программе вы решаете проблему размера n, сначала разрешая два подзадачи размера n/2. Или, в более общем плане, вы решаете проблему размера n сначала разрешив подзадачу размера k и один из размеров n-k, где 1 < k < n. Quicksort и mergesort - два примера такого рода проблем, которые может быть легко запрограммирован рекурсивно, но программа не так проста итеративно или с использованием только хвостовой рекурсии. (Вы, по сути, должны имитировать рекурсию с использованием явного стек.)

  • В динамическом программировании вы решаете проблему размера n, сначала разрешая все подзадачи всех размеров k, где k<n. Поиск кратчайшего пути от одного точка в другом на Лондонском метро - пример такого рода проблема. (The London Underground - многосвязный граф, и вы решить проблему, сначала найдя все точки, для которых кратчайший путь 1 остановка, тогда для которой кратчайший путь 2 остановки и т.д. и т.д.)

Только первый вид программы имеет простое преобразование в хвосто-рекурсивную форму.

Ответ 3

Любой рекурсивный алгоритм может быть переписан как итеративный алгоритм (возможно, требуется стек или список), и итеративные алгоритмы всегда могут быть переписаны как хвосто-рекурсивные алгоритмы, поэтому я считаю, что любое рекурсивное решение может каким-то образом быть преобразовано в хвост -рекурсивное решение.

(В комментариях Паскаль Куок указывает, что любой алгоритм может быть преобразован в стиль продолжения прохождения.)

Обратите внимание, что только потому, что что-то является хвостовым рекурсивным, это не означает, что его использование памяти является постоянным. Это просто означает, что стек обратного вызова не растет.

Ответ 4

Вы не можете делать все в O (1) пространстве (теорема пространственной иерархии). Если вы настаиваете на использовании хвостовой рекурсии, вы можете сохранить стек вызовов как один из аргументов. Очевидно, это ничего не меняет; где-то внутри, есть стек вызовов, вы просто делаете его явно видимым.

Если это так, в один прекрасный день функциональные компиляторы и интерпретаторы могут быть достаточно интеллектуальными для автоматического преобразования?

Такое преобразование не уменьшит пространственную сложность.

Как прокомментировал Паскаль Куок, другим способом является использование CPS; все вызовы являются хвостовыми рекурсивными.

Ответ 5

Я не думаю, что что-то вроде tak может быть реализовано с использованием только хвостовых вызовов. (не разрешая продолжения)