Чому функції в Ocaml / F # не є рекурсивними за замовчуванням?


104

Чому саме функції F # і Ocaml (і, можливо, інших мов) за замовчуванням не є рекурсивними?

Іншими словами, чому мовні дизайнери вирішили, що було б чітко змусити вас ввести recдекларацію типу:

let rec foo ... = ...

а не надавати функції рекурсивної можливості за замовчуванням? Чому необхідність явної recконструкції?


Відповіді:


87

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

У французькому сімействі мов CAML (включаючи OCaml) функції за замовчуванням не є рекурсивними. Цей вибір дозволяє легко замінити визначення функцій (та змінних), використовуючи letці мови, тому що ви можете посилатися на попереднє визначення всередині тіла нового визначення. F # успадкував цей синтаксис від OCaml.

Наприклад, витіснення функції pпри обчисленні ентропії Шеннона послідовності в OCaml:

let shannon fold p =
  let p x = p x *. log(p x) /. log 2.0 in
  let p t x = t +. p x in
  -. fold p 0.0

Зверніть увагу, як аргумент pфункції вищого порядку shannonвитісняється іншим pу першому рядку тіла, а потім в іншомуp у другій лінії тіла.

І навпаки, британська гілка SML сімейства мов ML взяла інший вибір, і funфункції, пов'язані з SML, є рекурсивними за замовчуванням. Коли більшість визначень функцій не потребують доступу до попередніх прив'язок назви їх функції, це призводить до більш простого коду. Тим НЕ менше, витіснили функції зроблені використовувати різні імена ( f1, і f2т.д.) , які забруднюють область застосування і дозволяє випадково апелює до неправильної «версії» функції. І тепер існує розбіжність між неявно-рекурсивно- funзв’язаними функціями та нерекурсивно- valзв’язаними функціями.

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

Зауважте, що відповіді Ганеша та Едді - це червоні оселедці. Вони пояснили, чому групи функцій не можуть бути розміщені всередині гіганта, let rec ... and ...оскільки це впливає на генералізацію змінних типів. Це не має нічого спільного з recтим, що за умовчанням є SML, але не OCaml.


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

"... автоматично трактується як взаємно рекурсивний, як це робить більшість інших мов". BASIC, C, C ++, Clojure, Erlang, F #, Factor, Forth, Fortran, Groovy, OCaml, Pascal, Smalltalk та Standard ML.
JD

3
C / C ++ вимагають тільки прототипів для визначення прямого напрямку, що насправді не стосується явного маркування рекурсії. Java, C # і Perl, безумовно, мають неявну рекурсію. Ми могли б вступити в нескінченну дискусію щодо значення "більшості" та важливості кожної мови, тому давайте просто поговоримо з "дуже багатьма" іншими мовами.
GS - Вибачте Моніку

3
"C / C ++ вимагають тільки прототипів для визначення прямого напрямку, що насправді не стосується явного маркування рекурсії". Тільки в особливому випадку саморекурсії. У загальному випадку взаємної рекурсії форвардні декларації є обов'язковими як для C, так і для C ++.
JD

2
Насправді прямі декларації не потрібні для C ++ у діапазонах класів, тобто статичні методи добре дзвонити один одному без будь-яких декларацій.
polkovnikov.ph

52

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

Якщо у вас є визначення let f x = x, ви очікуєте, що воно має тип 'a -> 'aі буде застосовано для різних 'aтипів у різних точках. Але в рівній мірі, якщо ви пишете let g x = (x + 1) + ..., ви б розраховували, що xдо вас ставляться як intдо решти тіла g.

Спосіб, яким можна дійти висновку Хіндлі-Мілнера, полягає в явному кроці узагальнення . У певні моменти при обробці вашої програми система типів зупиняється і каже "нормально, типи цих визначень будуть узагальнені в цей момент, так що коли хтось їх використовує, будь-які змінні вільного типу в їхньому вигляді будуть свіжими ідентифікованими, і таким чином не заважатиме жодним іншим способам використання цього визначення. "

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

Отже, враховуючи, що перевіряючий тип повинен знати про те, які набори визначень взаємно рекурсивні, що це може зробити? Одна з можливостей - це просто зробити аналіз залежності всіх визначень у межах сфери та переупорядкувати їх на найменші можливі групи. Haskell насправді робить це, але в таких мовах, як F # (і OCaml і SML), які мають необмежені побічні ефекти, це погана ідея, тому що вона може змінити побічні ефекти також. Тому замість цього він просить користувача чітко позначити, які визначення є взаємно рекурсивними, і, таким чином, шляхом розширення, де має відбуватися узагальнення.


3
Помилка, ні. Ваш перший абзац невірний (ви говорите про явне використання "і", а не "rec"), і, отже, решта не має значення.
JD

5
Я ніколи не був задоволений цією вимогою. Дякую за пояснення. Ще одна причина, чому Haskell перевершує дизайн.
Бент Расмуссен

9
НЕМАЄ!!!! ЯК МОЖУТЬ ЦИЙ ДІЙ ?! Ця відповідь явно неправильна! Будь ласка, прочитайте відповідь Гарропа нижче або ознайомтесь із визначенням стандартного ML (Milner, Tofte, Harper, MacQueen - 1997) [с.24]
lambdapower

9
Як я вже говорив у своїй відповіді, питання виводу типу є однією з причин необхідності повторного використання, а не єдиною причиною. Відповідь Джона також є дуже вагомою відповіддю (крім звичайного коментаря до Сніда про Haskell); Я не думаю, що вони в опозиції.
GS - Вибачте Моніку

16
"питання висновку типу є однією з причин необхідності повторного запису". Те, що OCaml вимагає, recале SML, не є очевидним протилежним прикладом. Якби висновок типу був проблемою з описаних вами причин, OCaml та SML не могли обрати різні рішення, як вони. Причина, звичайно, в тому, що ви говорите про те and, щоб зробити Haskell актуальним.
JD

10

Є дві ключові причини, що це гарна ідея:

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

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


4

Деякі здогадки:

  • letвикористовується не тільки для прив'язки функцій, але й інших регулярних значень. Більшість форм значень не можуть бути рекурсивними. Дозволені певні форми рекурсивних значень (наприклад, функції, ледачі вирази тощо), тому для цього потрібен явний синтаксис.
  • Можливо, буде простіше оптимізувати нерекурсивні функції
  • Закриття, створене під час створення рекурсивної функції, має включати запис, який вказує на саму функцію (щоб функція могла рекурсивно називати себе), що робить рекурсивні закриття складнішими, ніж нерекурсивні закриття. Тому може бути приємно створити простіші нерекурсивні закриття, коли вам не потрібна рекурсія
  • Це дозволяє визначити функцію з точки зору попередньо визначеної функції або значення однойменного значення; хоча я думаю, що це погана практика
  • Додаткова безпека? Переконується, що ви робите те, що задумали. Наприклад, якщо ви не збираєтесь бути рекурсивним, але ви випадково використали ім’я всередині функції з тим же ім'ям, що і сама функція, воно, швидше за все, скаржиться (якщо ім'я не було визначено раніше)
  • letКонструкція аналогічна letконструкції в Ліспі і схемою; які є нерекурсивними. В letrecсхемі є окрема конструкція для рекурсивного давайте

"Більшість форм значень не можуть бути рекурсивними. Певні форми рекурсивних значень дозволені (наприклад, функції, ледачі вирази тощо), тому для цього потрібен явний синтаксис". Це справедливо для F #, але я не впевнений, наскільки це правда для OCaml, де ви можете це зробити let rec xs = 0::ys and ys = 1::xs.
JD

4

Враховуючи це:

let f x = ... and g y = ...;;

Порівняйте:

let f a = f (g a)

З цим:

let rec f a = f (g a)

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

Це сказало, що це стиль дизайнера мови. Просто піди з цим.


1

Велика частина цього полягає в тому, що це дає програмісту більше контролю над складністю їх локальних областей. Спектр let, let*а let recпропозиція зростає рівень як потужності і вартості. let*і let recпо суті є вкладеними версіями простогоlet , тому використання будь-якої з них є дорожчою. Ця класифікація дозволяє мікромініструвати оптимізацію вашої програми, оскільки ви можете вибрати, який рівень дозволу вам потрібен для завдання. Якщо вам не потрібна рекурсія або можливість посилатися на попередні прив'язки, ви можете повернутися до простого дозволу, щоб зберегти трохи продуктивності.

Це схоже на градуйовані предикати рівності в схемі. (тобто eq?, eqv?і equal?)

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