Ці інші відповіді дещо оманливі. Я погоджуюся, що вони містять деталі щодо імплементації, які можуть пояснити цю невідповідність, але вони завищують цю справу. Як правильно запропонував jmite, вони орієнтовані на реалізацію на розбиті реалізації викликів / рекурсії функцій. Багато мов реалізують цикли за допомогою рекурсії, тому певи явно не будуть швидшими в цих мовах. Рекурсія нічим не менш ефективна, ніж циклічне (якщо обидва застосовні) теоретично. Дозвольте мені навести конспект до статті Гая Стіла 1977 року, що розкриває міф про "дорогий процедурний дзвінок" або, що впровадження процедури вважається шкідливим або, Лямбда: кінцева ГОТА
Фольклор стверджує, що заяви GOTO є "дешевими", а виклики процедур - "дорогими". Цей міф значною мірою є результатом погано розроблених мовних реалізацій. Розглянуто історичне зростання цього міфу. Обговорюються як теоретичні ідеї, так і існуюча реалізація, що розкриває цей міф. Показано, що необмежене використання процедурних викликів дозволяє велику стилістичну свободу. Зокрема, будь-яка блок-схема може бути записана як "структурована" програма без введення додаткових змінних. Складність із заявою GOTO та викликом процедури характеризується як конфлікт між абстрактними поняттями програмування та конкретними мовними конструкціями.
"Конфлікт між поняттями абстрактного програмування та конкретними мовними конструкціями" видно з того, що більшість теоретичних моделей, наприклад, нетипізованого числення лямбда , не мають стека . Звичайно, цей конфлікт не є необхідним, як показано у статті, і це також демонструється мовами, які не мають іншого механізму ітерації, крім рекурсії, наприклад, Haskell.
fix
fix f x = f (fix f) x
( λ x . m) N⇝ М[ N/ х][ N/ х]хМN⇝
Тепер для прикладу. Визначте fact
як
fact = fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1
Ось оцінка fact 3
, де для компактності я буду використовувати g
як синонім для fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1))
, тобто fact = g 1
. Це не впливає на мій аргумент.
fact 3
~> g 1 3
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1 3
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 1 3
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 1 3
~> (λn.if n == 0 then 1 else g (1*n) (n-1)) 3
~> if 3 == 0 then 1 else g (1*3) (3-1)
~> g (1*3) (3-1)
~> g 3 2
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 3 2
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 3 2
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 3 2
~> (λn.if n == 0 then 3 else g (3*n) (n-1)) 2
~> if 2 == 0 then 3 else g (3*2) (2-1)
~> g (3*2) (2-1)
~> g 6 1
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 1
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 1
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 1
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 1
~> if 1 == 0 then 6 else g (6*1) (1-1)
~> g (6*1) (1-1)
~> g 6 0
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 0
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 0
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 0
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 0
~> if 0 == 0 then 6 else g (6*0) (0-1)
~> 6
Ви можете бачити з форми, навіть не дивлячись на деталі, що немає зростання і для кожної ітерації потрібно однакова кількість місця. (Технічно, числовий результат зростає, що неминуче і так само справедливо для while
циклу.) Я спростовую вас, щоб вказати на безмежно зростаючий "стек" тут.
Здається, архетипна семантика обчислення лямбда вже робить те, що прийнято називати «оптимізацією хвостових викликів». Звичайно, ніякої «оптимізації» тут не відбувається. Тут немає спеціальних правил для "хвостових" дзвінків на відміну від "звичайних" дзвінків. З цієї причини важко дати "абстрактну" характеристику того, що робиться "оптимізація" хвостового виклику, як у багатьох абстрактних характеристиках семантики виклику функцій, для "оптимізації" хвостового виклику немає нічого!
Те, що аналогічне визначення fact
багатьох мов "переповнення стека", є невдачею цих мов правильно реалізувати семантику виклику функцій. (У деяких мовах є виправдання.) Ситуація приблизно аналогічна мовній реалізації, яка реалізує масиви із пов'язаними списками. Індексація в такі "масиви" буде операцією O (n), яка не відповідає очікуванню масивів. Якби я зробив окрему реалізацію мови, яка використовувала справжні масиви замість пов'язаних списків, ви б не сказали, що я здійснив "оптимізацію доступу до масиву", ви б сказали, що я виправив зламану реалізацію масивів.
Отже, відповідаючи на відповідь Ведрака. Стеки не є "основоположними" для рекурсії . У міру того, як поведінка типу «стека» виникає в ході оцінювання, це може статися лише у випадках, коли цикли (без допоміжної структури даних) не були застосовні в першу чергу! Інакше кажучи, я можу реалізувати петлі з рекурсією з абсолютно однаковими характеристиками. Дійсно, схема і SML містять циклічні конструкції, але обидва вони визначають ті, що стосуються рекурсії (і, принаймні, у схемі, do
часто реалізується як макрос, який переростає в рекурсивні дзвінки.) Так само, за відповідь Йохана, нічого не говорить про компілятор повинен випустити збірку, описану Йоханом для рекурсії. Дійсно,точно така ж збірка, чи використовуєте ви циклі або рекурсію. Єдиний раз, коли компілятор зобов’язаний буде випускати збірку, як те, що описує Йохан, - це коли ти робиш щось, що не може бути виразним циклом. Як викладено в статті Стіла та продемонстровано фактичною практикою таких мов, як Haskell, Scheme та SML, не "надзвичайно рідко", що хвостові дзвінки можна "оптимізувати", вони завжди можутьбути "оптимізованим". Від того, чи буде певне використання рекурсії працювати в постійному просторі, залежить від того, як це написано, але обмеження, які потрібно застосувати, щоб зробити це можливим, - це обмеження, які вам знадобляться, щоб відповідати вашій проблемі у формі циклу. (Насправді вони менш суворі. Існують проблеми, такі як машини кодування, які більш чітко та ефективно обробляються за допомогою викликів хвостів на відміну від циклів, які потребують допоміжних змінних.) Знову ж таки, єдиний час рекурсії вимагає більше роботи коли ваш код все одно не є циклом.
Думаю, Йоган має на увазі компілятори C, які мають довільні обмеження щодо того, коли він буде виконувати хвостовий виклик "оптимізація". Іохан також, мабуть, посилається на такі мови, як C ++ та Rust, коли він говорить про "мови з керованими типами". RAII ідіома від C ++ і присутній в іржі , а також роблять речі , які зовні виглядають як хвостові виклики, а НЕ хвіст викликів (бо «руйнівники» ще потрібно назвати). Були пропозиції використовувати інший синтаксис, щоб увімкнути дещо іншу семантику, яка дозволила б рекурсувати хвіст (а саме деструктори виклику ранішеостаточний виклик хвоста і очевидно забороняють доступ до "знищених" об'єктів). (Збір сміття не має такої проблеми, і всі Haskell, SML та Scheme - це зібрані сміття мови.) У зовсім іншому ключі деякі мови, такі як Smalltalk, виставляють "стек" як першокласний об'єкт у цих випадків "стек" більше не є детальною інформацією про реалізацію, хоча це не виключає наявності окремих типів викликів з різною семантикою. (Java каже, що це не може через те, що він обробляє деякі аспекти безпеки, але це насправді помилково .)
На практиці поширеність порушених реалізацій викликів функцій відбувається з трьох основних факторів. По-перше, багато мов успадковують зламану реалізацію від мови їхньої реалізації (зазвичай, C). По-друге, детерміноване управління ресурсами є приємним і робить проблему складнішою, хоча це пропонує лише декілька мов. По-третє, і, на мій досвід, більшість людей переймається тим, що вони хочуть слідів стека, коли виникають помилки з метою налагодження. Тільки друга причина - це теоретично мотивована.