Почему функции в Ocaml/F # не рекурсивны по умолчанию?

Почему функции F # и Ocaml (и, возможно, другие языки) по умолчанию не рекурсивные?

Другими словами, почему разработчики языка решили, что было бы хорошей идеей явно ввести тип rec в объявление вроде:

let rec foo ... = ...

и не дать функции рекурсивной функции по умолчанию? Зачем нужна явная конструкция rec?

Ответ 1

Французские и британские потомки оригинального ML сделали разные варианты, и их выбор был унаследован через десятилетия до современных вариантов. Так что это просто наследие, но оно влияет на идиомы на этих языках.

Функции по умолчанию не являются рекурсивными во французском семействе языков 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 взяла другой выбор, а функции SML fun - по умолчанию являются рекурсивными. Когда большинству определений функций не нужен доступ к предыдущим привязкам имени их функции, это приводит к более простому коду. Однако введенные функции используются для использования разных имен (f1, f2 и т.д.), Которые загрязняют область действия и позволяют случайно вызвать неправильную "версию" функции. И теперь существует расхождение между неявно-рекурсивными fun -связанными функциями и нерекурсивными функциями val.

Haskell позволяет выводить зависимости между определениями, ограничивая их чистоту. Это делает образцы игрушек более простыми, но приносит серьезные издержки в другом месте.

Обратите внимание, что ответы Ганеша и Эдди - это красные сельди. Они объяснили, почему группы функций не могут быть размещены внутри гигантского let rec ... and ..., потому что это влияет на генерацию переменных типа. Это не имеет ничего общего с rec значением по умолчанию в SML, но не OCaml.

Ответ 2

Одной из важных причин явного использования rec является использование вывода типа Hindley-Milner, лежащего в основе всех статически типизированных языков программирования (хотя и измененных и расширенных различными способами).

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

То, каким образом вывод Хиндли-Милнера имеет дело с этим различием, заключается в явном шаге обобщения. В определенных случаях при обработке вашей программы система типов останавливается и говорит "хорошо, типы этих определений будут обобщены в этот момент, так что, когда кто-то их использует, любые свободные переменные типа в их типе будут только что созданы, и, таким образом, не будет мешать другим применениям этого определения".

Оказывается, что разумное место для этого обобщения - это проверка взаимно-рекурсивного набора функций. Раньше, и вы слишком много обобщаете, что приводит к ситуациям, когда типы могут действительно сталкиваться. Позднее, и вы слишком мало обобщаете, создавая определения, которые нельзя использовать с несколькими экземплярами типов.

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

Ответ 3

Есть две основные причины, по которым это хорошая идея:

Во-первых, если вы включаете рекурсивные определения, вы не можете ссылаться на предыдущую привязку значения с тем же именем. Это часто является полезной идиомой, когда вы делаете что-то вроде расширения существующего модуля.

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

Ответ 4

Некоторые предположения:

  • let используется не только для привязки функций, но и для других регулярных значений. Большинство форм значений не могут быть рекурсивными. Разрешены определенные формы рекурсивных значений (например, функции, ленивые выражения и т.д.), Поэтому для указания этого требуется явный синтаксис.
  • Может быть проще оптимизировать нерекурсивные функции
  • Закрытие, созданное при создании рекурсивной функции, должно включать запись, указывающую на саму функцию (так что функция может рекурсивно вызывать себя), что делает рекурсивное закрытие более сложным, чем нерекурсивное закрытие. Поэтому было бы неплохо иметь возможность создавать более простые нерекурсивные замыкания, когда вам не нужна рекурсия.
  • Он позволяет вам определить функцию в терминах ранее определенной функции или значения с тем же именем; хотя я думаю, что это плохая практика.
  • Дополнительная безопасность? Уверен, что вы делаете то, что планируете. например Если вы не хотите, чтобы он был рекурсивным, но вы случайно использовали имя внутри функции с тем же именем, что и сама функция, она, скорее всего, будет жаловаться (если только имя не было определено ранее)
  • Конструкция let аналогична конструкции let в Lisp и схеме; которые являются нерекурсивными. Существует отдельная конструкция letrec в Схеме для рекурсивного let

Ответ 5

Учитывая это:

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.

Тем не менее, это стиль дизайнерского стиля. Просто пойдите с ним.

Ответ 6

Большая его часть заключается в том, что он дает программисту больше контроля над сложностью локальных областей. Спектр let, let* и let rec обеспечивает растущий уровень мощности и стоимости. let* и let rec по сути являются вложенными версиями простого let, поэтому использование одного из них является более дорогостоящим. Эта сортировка позволяет вам оптимизировать вашу программу, так как вы можете выбрать, какой уровень вам нужен для выполнения этой задачи. Если вам не нужна рекурсия или возможность ссылаться на предыдущие привязки, вы можете вернуться к простому, чтобы сохранить немного производительности.

Он похож на градуированные предикаты равенства в Схеме. (т.е. eq?, eqv? и equal?)