Це загальний спосіб перетворення будь-якої рекурсивної процедури в хвостову рекурсію?


13

Здається, я знайшов загальний спосіб перетворити будь-яку рекурсивну процедуру в хвостову рекурсію:

  1. Визначте допоміжну допоміжну процедуру з додатковим параметром "результат".
  2. Застосуйте те, що було б застосовано до зворотного значення процедури до цього параметра.
  3. Для початку зателефонуйте на цю процедуру допомоги. Початкове значення параметра "результат" - це значення для точки виходу рекурсивного процесу, так що отриманий ітераційний процес починається з того місця, де рекурсивний процес починає скорочуватися.

Наприклад, ось оригінальна рекурсивна процедура, яку потрібно перетворити ( SICP, вправа 1.17 ):

(define (fast-multiply a b)
  (define (double num)
    (* num 2))
  (define (half num)
    (/ num 2))
  (cond ((= b 0) 0)
        ((even? b) (double (fast-multiply a (half b))))
        (else (+ (fast-multiply a (- b 1)) a))))

Ось перетворена хвостово-рекурсивна процедура ( вправа SICP 1.18 ):

(define (fast-multiply a b)
  (define (double n)
    (* n 2))
  (define (half n)
    (/ n 2))
  (define (multi-iter a b product)
    (cond ((= b 0) product)
          ((even? b) (multi-iter a (half b) (double product)))
          (else (multi-iter a (- b 1) (+ product a)))))
  (multi-iter a b 0))

Хтось може це довести чи спростувати?


1
O(logn)

Друга думка: вибір bсилою 2 показує, що спочатку встановлення product0 не зовсім правильне; але змінити його на 1 не працює, коли bце непарно. Можливо, вам потрібні 2 різні параметри акумулятора?
j_random_hacker

3
Ви насправді не визначили трансформацію не хвостового рекурсивного визначення, додавання якогось параметра результату та використання його для накопичення є досить розпливчастим і навряд чи узагальнює складніші випадки, наприклад, обхід дерев, де у вас є два рекурсивні виклики. Більш точна ідея "продовження" існує, хоча ви виконуєте частину роботи, а потім дозволяєте функції "продовження" взяти на себе, отримуючи в якості параметра роботу, яку ви виконували досі. Він називається стилем продовження передачі (cps), див. En.wikipedia.org/wiki/Continuation-passing_style .
Аріель

4
Ці слайди fsl.cs.illinois.edu/images/d/d5/CS422-Fall-2006-13.pdf містять опис перетворення cps , в якому ви приймаєте деякий довільний вираз (можливо, з визначеннями функцій із викликами без хвоста) і перетворити його в еквівалентний вираз лише з викликами хвоста.
Аріель

@j_random_hacker Так, я можу побачити, що моя "перетворена" процедура насправді неправильна ...
nalzok

Відповіді:


12

Ваш опис алгоритму дійсно занадто розпливчастий, щоб оцінити його на даний момент. Але, ось деякі речі слід врахувати.

CPS

Насправді, існує спосіб перетворити будь-який код у форму, яка використовує лише хвостові дзвінки. Це перетворення CPS. CPS ( стиль продовження передачі ) - це форма вираження коду, передаючи кожній функції продовження. Продовження - це абстрактне поняття, що представляє "решту обчислень". У коді, вираженому у формі CPS, природний спосіб повторити продовження - це функція, яка приймає значення. У CPS замість функції, яка повертає значення, вона замість цього застосовує функцію, що представляє поточне продовження, до того, що функція "повертається".

Наприклад, розглянемо таку функцію:

(lambda (a b c d)
  (+ (- a b) (* c d)))

Це можна виразити в CPS так:

(lambda (k a b c d)
  (- (lambda (v1)
       (* (lambda (v2)
            (+ k v1 v2))
          a b))
     c d))

Це некрасиво і часто повільно, але має певні переваги:

  • Перетворення може бути повністю автоматизованим. Тому немає необхідності писати (або бачити) код у формі CPS.
  • У поєднанні з громом та батутом , це може бути використано для забезпечення оптимізації виклику хвостами мовами, які не забезпечують оптимізацію хвостових викликів. (Оптимізація хвостових викликів безпосередньо хвостово-рекурсивних функцій може бути здійснена за допомогою інших засобів, таких як перетворення рекурсивного виклику в цикл. Але непряма рекурсія не настільки тривіальна для перетворення таким чином.)
  • З CPS продовження перетворюються на першокласні об'єкти. Оскільки продовження є сутністю управління, це дозволяє практично будь-якому оператору управління реалізовуватися як бібліотека, не вимагаючи особливої ​​підтримки з боку мови. Наприклад, гото, винятки та кооперативна нитка можуть бути змодельовані за допомогою продовження.

ТСО

Мені здається, що єдиною причиною турбуватися з рекурсією хвоста (або взагалі закликами хвоста) є цілі оптимізації хвостових викликів (TCO). Тож я думаю, що краще задати питання: "чи мій код вихідної трансформації, який можна оптимізувати?"

Якщо ми ще раз розглянемо CPS, одна з її характеристик полягає в тому, що код, виражений у CPS, складається виключно з хвостових викликів. Оскільки все є хвостиком, нам не потрібно зберігати точку повернення до стеку. Таким чином, весь код у формі CPS повинен бути оптимізований для зворотного дзвінка, правда?

Ну, не зовсім. Розумієте, хоча може здатися, що ми усунули стек, все, що ми зробили, - це просто змінити спосіб їх представлення. Стек тепер є частиною закриття, що представляє собою продовження. Таким чином, CPS не робить магічним чином оптимізований весь наш код.

Отже, якщо CPS не може зробити все TCO, чи існує трансформація спеціально для прямої рекурсії, яка може? Ні, не взагалі. Деякі рекурсії є лінійними, але деякі - ні. Нелінійні (наприклад, дерево) рекурсії просто повинні десь підтримувати змінну кількість стану.


це трохи заплутано, коли в підрозділі " TCO ", коли ви говорите "оптимізований дзвінок", ви насправді маєте на увазі "при постійному використанні пам'яті". Те, що використання динамічної пам’яті не є постійним, все ще не перешкоджає тому, що дзвінки справді є хвостими, і немає необмеженого зростання використання стека . SICP називає такі обчислення "ітераційними", тому мовлення може бути кращим формулюванням (для мене, хоча "це TCO, це все ще не робить його ітераційним").
Буде Несс

@WillNess У нас все ще є стек викликів, він просто представлений інакше. Структура не змінюється лише тому, що ми використовуємо купу, а не апаратний стек. Зрештою, існує маса структур даних, заснованих на динамічній купі пам’яті, яка має «стек» у своєму імені.
Натан Девіс

Єдиний момент тут - це те, що деякі мови мають обмежені обмеження на використання стека викликів.
Буде Несс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.