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

Только для справки существует другой проект, названный MacroSchmacro, который делает макросы Eclipse, но это не записывает много важных вещей (как поиск для навигации). Это также чрезвычайно медленно.

47
задан Carl Smotricz 11 December 2009 в 15:26
поделиться

5 ответов

Да, на самом деле вы можете взять какой-нибудь код и преобразовать каждый вызов функции - и каждый возврат - в хвостовой зов. В результате вы получите стиль передачи продолжения, или 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 говорит, что вы можете ' t делать все в пространстве O (1), и это правильно; здесь каждое продолжение - это замыкание, которое занимает некоторую память.)

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

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

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

47
ответ дан 26 November 2019 в 19:37
поделиться

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

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

Вот пример функции, которая является «рекурсивной» (на самом деле Это' Мы можем создать хвостовую рекурсивную версию, поместив произведение в параметр накопления:

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 путем первого решения двух subproblems of size n/2. Or, more generally, you solve a problem of size n by first solving a subproblem of size k and one of size n-k, where 1 < k < n. Quicksort and mergesort are two examples of this kind of problem, which can easily be programmed recursively, but is not so easy to program iteratively or using only tail recursion. (You essentially have to simulate recursion using an explicit stack.)

  • In dynamic programming, you solve a problem of size n by first solving all subproblems of all sizes k, where k. Finding the shortest route from one point to another on the London Underground is an example of this kind of problem. (The London Underground is a multiply-connected graph, and you solve the problem by first finding all points for which the shortest path is 1 stop, then for which the shortest path is 2 stops, etc etc.)

Only the first kind of program has a simple transformation into tail-recursive form.

26
ответ дан 26 November 2019 в 19:37
поделиться

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

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

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

11
ответ дан 26 November 2019 в 19:37
поделиться

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

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

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

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

9
ответ дан 26 November 2019 в 19:37
поделиться

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

1
ответ дан 26 November 2019 в 19:37
поделиться
Другие вопросы по тегам:

Похожие вопросы: