Як зрозуміти цей код рекурсії?


12

Я знайшов цей код у посібнику, що An Introduction to Programming in Emacs Lispдемонструє рекурсію за допомогою condфункції, щоб дізнатись кількість камінчиків на основі введеної кількості рядків, тобто якщо рядки = 2, то гальки повинні бути 3, якщо 4 ряди, то це має бути 10 камінчиків там.

(defun triangle-using-cond (number)
  (cond ((<= number 0) 0)
        ((= number 1) 1)
        ((> number 1)
         (+ number (triangle-using-cond (1- number))))))

оцінити до 10 після проходження аргументу 4:

(triangle-using-cond 4)

Посібник не пояснив чітко, що відбувається на кожному кроці в цьому прикладі коду, і я не міг зрозуміти, як працює тут рекурсія. Чи можете ви допомогти мені зрозуміти механіку поетапно, що відбувається у кожному випадку?


Я дозволю комусь іншому допомогти вам у слові "рекурсія" (тому що я думаю, що це щось інше, ніж у цьому контексті), або краще поясніть, що я збираюся написати: (a) якщо число менше або рівне до 0, тоді 0; (b) якщо число дорівнює 1, то 1; (c) якщо число більше 1, то додайте число до значення, поверненого функцією, triangle-using-condаргумент на 1 менше, ніж будь-яке число. Умови йдуть у порядку a, b, а потім c - усе, що спочатку відповідає, саме там долар зупиняється.
законник

як зазвичай перекладач елісп оцінює від найглибшого до зовнішнього. Таким чином, 1-4 = 3. Тепер рекурсивний дзвінок буде (triangle-using-cond 3), але це закінчиться тим самим рекурсивним дзвінком знову і знову, поки він не потрапить на 1 умовний, правда? що буде далі?
докторантура

О, я бачу - функція повторно використовує себе на кроці 3 - добре, добре.
законник

Цікаво, який би був результат (triangle-using-cond 3)?
докторантура

2
nb Функція 1-має особливо оманливу назву, особливо якщо ви читаєте дзвінок так, ніби він був інфіксованим позначенням. Він повертає свій аргумент мінус один; НЕ один мінус аргумент.
phils

Відповіді:


14

Використання "налагодження printf"

Ви можете дозволити Emacs допомогти вам зрозуміти, змінивши визначення функції:

(defun triangle-using-cond (number)
  (message (format "called with %d" number))
  (cond ((<= number 0) 0)
        ((= number 1) 1)
        ((> number 1)
         (+ number (triangle-using-cond (1- number))))))

Просто додайте (message ...)десь, щоб слід було надрукувати слід до *Messages*буфера.

Використання Edebug

Помістіть точку в будь-якому місці визначення функції та натисніть C-u C-M-xна "інструмент". Потім оцініть функцію, наприклад, поставивши крапку після (triangle-using-cond 3)та натиснувши C-x C-e.

Зараз ви перебуваєте в режимі Edebug. Натисніть пробіл, щоб перейти через функцію. Проміжні значення кожного виразу відображаються в області ехо. Для виходу з режиму Edebug просто натисніть q. Щоб видалити інструментарій, поставте крапку десь усередині визначення та натисніть, C-M-xщоб переоцінити визначення.

Використання стандартного відладчика Emacs

M-x debug-on-entry triangle-using-cond, тоді при triangle-using-condвиклику вас розміщують у відладчику (буфері *Backtrace*) Emacs .

Перегляньте оцінку за допомогою d(або cпропустіть будь-які нецікаві оцінки).

Для перегляду проміжного стану (змінних значень тощо) ви можете використовувати eбудь-коли. Вам буде запропоновано ввести сексоп для оцінки, і результат оцінки надрукується.

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

Ви також можете вставити явні дзвінки для введення налагоджувача (більш-менш точок перерви) у довільних місцях у вихідному коді. Ви вставляєте (debug)або (debug nil SOME-SEXP-TO-EVALUATE). В останньому випадку при введенні налагоджувача SOME-SEXP-TO-EVALUATEоцінюється і результат друкується. (Пам'ятайте, що ви можете вставити такий код у вихідний код і використовувати його C-M-xдля оцінки, а потім скасувати - не потрібно зберігати відредагований файл.)

Дивіться посібник Elisp, вузол Using Debuggerдля отримання додаткової інформації.

Рекурсія як петля

У будь-якому випадку, подумай про рекурсію як цикл. Визначено два випадки припинення: (<= number 0)та (= number 1). У цих випадках функція повертає просте число.

У рекурсивному випадку функція повертає суму цього числа та результат функції з number - 1. Зрештою, функція буде викликана з 1або числом, меншим або рівним нулю.

Отже, рекурсивний результат:

(+ number (+ (1- number) (+ (1- (1- number)) ... 1)

Візьмемо для прикладу (triangle-using-cond 4). Давайте накопичимо підсумковий вираз:

  • у першій ітерації numberє 4, тому слід (> number 1)гілка. Починаємо будувати вираз (+ 4 ...і викликаємо функцію за допомогою (1- 4), тобто (triangle-using-cond 3).

  • зараз numberє 3, і результат є (+ 3 (triangle-using-cond 2)). Загальний вираз результату - (+ 4 (+ 3 (triangle-using-cond 2))).

  • numberє 2зараз, тому вираз є(+ 4 (+ 3 (+ 2 (triangle-using-cond 1))))

  • numberв 1даний час, і ми беремо (= number 1)гілка, в результаті роздратування 1. Весь вираз є (+ 4 (+ 3 (+ 2 1))). Оцінювати , що зсередини , і ви отримаєте: (+ 4 (+ 3 3)), (+ 4 6)або просто 10.


3
Едебуг буде ще кращим. =)
Малабарба

як отримати слід, надрукований за допомогою message (...), натискання C-x C-eпросто показує кінцевий результат (10) нічого іншого? Я щось пропускаю?
докторантура

@Malabarba, як вчинити Edebugв дії?
докторантура

1
@dokto вдарив C-u C-M-xз точкою всередині функції, щоб усунути її. Потім просто запустіть функцію як нормально.
Малабарба

@docto (message ...)матеріал друкує в *Message*буфер.
rekado

6

Модель заміни для застосування процедури з SICP може пояснити алгоритм розуміння коду, як це.

Я написав код, щоб полегшити це також. lispy-flattenз lispy пакета це робить. Ось результат звернення lispy-flattenдо (triangle-using-cond 4):

(cond ((<= 4 0)
       0)
      ((= 4 1)
       1)
      ((> 4 1)
       (+ 4 (triangle-using-cond (1- 4)))))

Ви можете спростити наведене вираз просто:

(+ 4 (triangle-using-cond 3))

Потім ще раз сплющуйте:

(+ 4 (cond ((<= 3 0)
            0)
           ((= 3 1)
            1)
           ((> 3 1)
            (+ 3 (triangle-using-cond (1- 3))))))

Кінцевий результат:

(+ 4 (+ 3 (+ 2 1)))

3

Це не характерно для Emacs / Elisp, але якщо у вас є математичний досвід, то рекурсія - це як математична індукція . (Або якщо ви цього не зробите: тоді, коли ви навчитесь індукції, це як рекурсія!)

Почнемо з визначення:

(defun triangle-using-cond (number)
  (cond ((<= number 0) 0)
        ((= number 1) 1)
        ((> number 1)
         (+ number (triangle-using-cond (1- number))))))

Коли numberнемає 4, не виконується жодна з перших двох умов, тому вона оцінюється відповідно до третьої умови:
(triangle-using-cond 4)оцінюється як
(+ number (triangle-using-cond (1- number))), а саме як
(+ 4 (triangle-using-cond 3)).

Аналогічно
(triangle-using-cond 3)оцінюється як
(+ 3 (triangle-using-cond 2)).

Аналогічно (triangle-using-cond 2)оцінюється як
(+ 2 (triangle-using-cond 1)).

Але для (triangle-using-cond 1)другої умова виконується, і вона оцінюється як 1.

Порада для тих, хто навчається рекурсії: намагайтеся уникати

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

Якщо ви намагаєтеся переконати себе, чи (triangle-using-cond 4)повернеться правильна відповідь, просто припустіть, що (triangle-using-cond 3)поверне правильну відповідь, і перевірте, чи буде вона правильною у такому випадку. Звичайно, ви також повинні перевірити базовий випадок.


2

Етапи обчислення для вашого прикладу будуть такими:

(4 +               ;; step 1
   (3 +            ;; step 2
      (2 +         ;; step 3
         (1))))    ;; step 4
=> 10

Умова 0 насправді ніколи не виконується, оскільки 1 як вхід вже закінчує рекурсію.


(1)не є дійсним виразом.
rekado

1
Це оцінює просто чудово M-x calc. :-) Серйозно, хоча я мав на увазі показати розрахунок, а не оцінку Ліспа.
паприка

О, я навіть не помітив, що це (4 +замість (+ 4вашої відповіді ... :)
rekado

0

Я думаю, що це досить просто, вам не потрібен emacs lisp для цього, це лише математика початкової школи.

f (0) = 0

f (1) = 1

f (n) = f (n-1) + n, коли n> 1

тому f (5) = 5 + f (4) = 5 + 4 + f (3) = 5 + 4 + 3 + 2 + 1 + 0

Тепер це очевидно.


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