Чи кращі функціональні мови при рекурсії?


41

TL; DR: Чи функціональні мови справляються з рекурсією краще, ніж нефункціональні?

На даний момент я читаю Code Complete 2. У якийсь момент у книзі автор попереджає нас про рекурсію. Він каже, що цього слід уникати, коли це можливо, і що функції, що використовують рекурсію, як правило, менш ефективні, ніж рішення, що використовує петлі. Як приклад, автор написав функцію Java, використовуючи рекурсію для обчислення фактора таких чисел (можливо, це не зовсім так, оскільки у мене зараз немає книги зі мною):

public int factorial(int x) {
    if (x <= 0)
        return 1;
    else
        return x * factorial(x - 1);
}

Це представлено як погане рішення. Однак у функціональних мовах використання рекурсії часто є кращим способом вчинити. Наприклад, ось функціональна функція в Haskell за допомогою рекурсії:

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)

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

Тому в основному моє питання:

  • Чи функціональні мови справляються з рекурсією краще, ніж нефункціональні?

EDIT: Я знаю, що приклади, які я використав, не найкращі для ілюстрації мого питання. Я просто хотів зазначити, що Haskell (і функціональні мови взагалі) використовують рекурсію набагато частіше, ніж нефункціональні мови.


10
Справа в суті: багато функціональних мов сильно використовують оптимізацію хвостових викликів, тоді як дуже мало процедурних мов це роблять. Це означає, що рекурсія хвостового виклику набагато дешевша в цих функціональних мовах.
Йоахім Зауер

7
Насправді, дане вами визначення Haskell є досить поганим. factorial n = product [1..n]є більш лаконічним, ефективнішим і не переповнює стек для великих n(і якщо вам потрібна пам'ятка, потрібні зовсім інші варіанти). productвизначається в термінах деяких fold, який буде визначений рекурсивно, але з крайньою обережністю. Рекурсія є прийнятним рішенням більшу частину часу, але все одно просто зробити це неправильно / субоптимально.

1
@JoachimSauer - Трохи прикрасивши, ваш коментар дасть вагому відповідь.
Марк Бут

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

@delnan: Дякую за роз’яснення! Я редагую свою
праву

Відповіді:


36

Так, вони роблять, але не лише тому, що можуть , а тому, що мають .

Ключова концепція тут - чистота : чиста функція - це функція, яка не має побічних ефектів і не має стану. Мови функціонального програмування, як правило, охоплюють чистоту з багатьох причин, наприклад, міркування про код та уникнення неочевидних залежностей. Деякі мови, особливо Хаскелл, навіть заходять так далеко, що дозволяють лише чистий код; будь-які побічні ефекти, які може мати програма (наприклад, виконання вводу-виводу), переміщуються до нечистого виконання, зберігаючи мову чистою.

Відсутність побічних ефектів означає, що у вас не може бути лічильників циклу (оскільки лічильник циклу може становити змінний стан, а зміна такого стану було б побічним ефектом), тому найбільш ітеративної чистої функціональної мови можна отримати перегляд списку ( ця операція зазвичай називається foreachабо map). Рекурсія, однак, є природною відповідністю чистим функціональним програмуванням - для повторної повторної роботи не потрібно жодного стану, за винятком аргументів функції (лише для читання) та поверненого значення (лише для запису).

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


2
Для запису, усунення хвостових викликів не покладається на чистоту.
шарфридж

2
@scarfridge: Звичайно, це не так. Однак, коли чистота задана, компілятору набагато простіше переупорядкувати ваш код, щоб дозволити виклики хвоста.
тдаммери

GCC робить набагато кращу роботу з TCO, ніж GHC, тому що ви не можете виконати TCO в усьому створенні грона.
dan_waterworth

18

Ви порівнюєте рекурсію з ітерацією. Без усунення хвостового виклику ітерація дійсно ефективніша, оскільки немає додаткового виклику функції. Крім того, ітерація може тривати назавжди, тоді як можливо не вистачити місця у стеку через занадто багато функціональних викликів.

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

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


Я вже знаю, про що йдеться у відповідності з візерунками, і я думаю, що це надзвичайна особливість у Haskell, яку я сумую за мовами, якими я користуюся!
marco-fiset

@marcof Моя думка полягає в тому, що всі розмови про рекурсію проти ітерації не стосуються гладкості вашого зразка коду. Йдеться дійсно про відповідність шаблону та умовні умови. Можливо, я мав би поставити це у своїй відповіді.
chrisaycock

Так, я теж це зрозумів: P
marco-fiset

@chrisaycock: Чи можна було б бачити ітерацію хвостовою рекурсією, в якій всі змінні, використовувані в тілі циклу, є і аргументами, і зворотними значеннями рекурсивних викликів?
Джорджіо

@Giorgio: Так, змусьте вашу функцію приймати та повертати кордон одного типу.
Ericson2314

5

Технічно ні, але практично так.

Рекурсія набагато частіше зустрічається, коли ви використовуєте функціональний підхід до проблеми. Таким чином, мови, призначені для використання функціонального підходу, часто включають функції, які роблять рекурсію легшою / кращою / менш проблематичною. У верхній частині моєї голови є три поширених:

  1. Оптимізація виклику хвоста. Як вказують інші афіші, функціональні мови часто вимагають TCO.

  2. Ледача оцінка. Хаскелл (та ще кілька інших мов) ліниво оцінюються. Це затримує фактичну «роботу» методу до тих пір, поки це не буде потрібно. Це, як правило, призводить до створення більш рекурсивних структур даних і розширення рекурсивних методів роботи над ними.

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

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


1
Re: 1. Ранні повернення не мають нічого спільного з хвостовими дзвінками. Ви можете повернутися рано за допомогою хвостового дзвінка, і для "пізнього" повернення також є хвостовий виклик, і ви можете мати єдине просте вираження, коли рекурсивний виклик не знаходиться в положенні хвоста (див. Факторіальне визначення OP).

@delnan: Спасибі; рано і минуло досить багато часу, як я вивчив річ.
Теластин

1

Хаскелл та інші функціональні мови зазвичай використовують ледачу оцінку. Ця функція дозволяє записувати нескінченні рекурсивні функції.

Якщо ви записуєте рекурсивну функцію, не визначаючи базовий випадок, коли рекурсія закінчується, у вас виникають нескінченні виклики до цієї функції та stackoverflow.

Haskell також підтримує рекурсивні оптимізації викликів функцій. У Java кожен виклик функції буде складатись і викликати накладні витрати.

Так що так, функціональні мови справляються з рекурсією краще, ніж інші.


5
Хаскелл є одним з небагатьох нестриктних мов - все сімейство ML (окрім деяких дослідницьких спинофф, які додають лінь), усі популярні Lisps, Erlang тощо. Крім того , другий абзаци здається вимкнений - як правильно вказати в першому абзаці, лінощі робить дозволяє нескінченна рекурсія (прелюдія Haskell має дуже корисно forever a = a >> forever a, наприклад).

@deinan: Наскільки я знаю, SML / NJ також пропонує ледачу оцінку, але це доповнення до SML. Я також хотів назвати дві з кількох лінивих функціональних мов: Міранда та Чиста.
Джорджіо

1

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

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


1
(1) Така оптимізація застосовується лише в дуже конкретних випадках - приклад ОП - це не вони, і багато інших прямих функцій потребують додаткового обережності, щоб стати рекурсивними. (2) Справжня оптимізація хвостового дзвінка не лише оптимізує рекурсивні функції, але й видаляє простір з будь-якого виклику, за яким одразу йде повернення.

@delnan: (1) Так, дуже правда. У своєму первісному проекті цієї відповіді я зазначив, що :( (2) Так, але в контексті запитання я вважав, що це буде стороннім для згадування.
Стівен Еверс

Так, (2) - просто корисне доповнення (хоча воно і незамінне для стилю продовження), відповіді не повинні згадувати.

1

Ви хочете подивитися, що збирання сміття швидко, але стек швидше , документ про використання того, що програмісти C вважають «кучкою» для стекових фреймів у зібраному С. Я вважаю, що автор попрацював з Gcc, щоб зробити це . Це не є однозначною відповіддю, але це може допомогти вам зрозуміти деякі проблеми з рекурсією.

Мова програмування Alef , який раніше постачався разом із планом 9 з Bell Labs, мав заяву "ставати" (Див. Розділ 6.6.4 цього посилання ). Це свого роду явна оптимізація рекурсії виклику. "Але він використовує стек викликів!" аргумент проти рекурсії потенційно може бути усунений.


0

TL; DR: Так, вони роблять
Рекурсія - це головний інструмент функціонального програмування, і тому було зроблено багато роботи з оптимізації цих викликів. Наприклад, R5RS вимагає (у специфікації!), Щоб усі реалізації обробляли незв'язані хвостові рекурсійні виклики, не переживаючи програміста про переповнення стека. Для порівняння, за замовчуванням компілятор C не зробить навіть очевидну оптимізацію хвостових викликів (спробуйте рекурсивний зворотний зв’язок пов'язаного списку), і після деяких дзвінків програма припиниться (компілятор оптимізує, хоча, якщо ви використовуєте - O2).

Звичайно, у програмах, які жахливо написані, наприклад, відомий fibекспоненціальний приклад, у компілятора мало варіантів зробити свою "магію". Тому слід бути обережним, щоб не перешкоджати зусиллям компілятора в оптимізації.

EDIT: Під прикладом fib я маю на увазі наступне:

(define (fib n)
 (if (< n 3) 1 
  (+ (fib (- n 1)) (fib (- n 2)))
 )
)

0

Функціональні мови краще в двох дуже специфічних видах рекурсії: хвостова рекурсія та нескінченна рекурсія. Вони так само погані, як і інші мови при інших видах рекурсії, як ваш factorialприклад.

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

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

Наприклад, пропозиція Дельнана про factorial n = product [1..n]не тільки більш стислий і простіший для читання, він також дуже паралельний. Те ж саме для використання foldабо, reduceякщо у вашій мові вже не productвбудована. Рекурсія - це вирішення останнього рішення цієї проблеми. Основна причина, по якій ви бачите, що це вирішується рекурсивно в навчальних посібниках, - це стрибок перед тим, як дійти до кращих рішень, а не як приклад найкращої практики.

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