Могут ли программы на функциональных языках чаще иметь переполнение стека?

Я начинаю изучать ocaml и действительно ценю силу рекурсии в языке. Тем не менее, одна вещь, о которой я беспокоюсь, — это переполнение стека.

Если ocaml использует стек для вызовов функций, не переполнит ли он в конечном итоге стек? Например, если у меня есть следующая функция:

let rec sum x =
  if x > 1 then f(x - 1) + x
  else x;;

в конечном итоге это должно вызвать переполнение стека. Если бы я сделал то же самое на С++ (используя рекурсию), я знаю, что это переполнило бы меня.

Итак, мой вопрос: есть ли встроенные средства защиты для предотвращения переполнения стека функциональными языками? Если нет, то не менее ли они полезны, поскольку приведенный выше алгоритм суммирования, написанный в процедурном стиле с циклом for, может обрабатывать любое число (без учета целочисленного переполнения)?


comment
Привет! это название сайта.   -  person kornfridge    schedule 06.04.2012


Ответы (5)


Все (достойные реализации ;-) функциональных языков оптимизируют хвостовую рекурсию, но это не то, что вы делаете здесь, поскольку рекурсивный вызов не является ПОСЛЕДНЕЙ операцией (за ним должно следовать добавление).

Итак, вскоре вы научитесь использовать вспомогательную функцию, которая является хвостовой рекурсией (и принимает в качестве аргумента текущую сумму, накапливаемую), чтобы оптимизатор мог выполнять свою работу, то есть за вычетом возможного синтаксиса O'Caml, в котором я заржавел:

let sum x =
  aux(x)(0);;

let rec aux x accum =
  if x > 1 then aux(x - 1)(accum + x)
  else (accum + x);;

Здесь сумма возникает как АРГУМЕНТ для рекурсивного вызова, т. е. ДО самой рекурсии, и поэтому может начаться хвостовая оптимизация (поскольку рекурсия — это последнее, что должно произойти!).

person Alex Martelli    schedule 18.08.2009
comment
OCaml не использует запятые для разделения аргументов функции. Он использует пробелы. Любые аргументы, являющиеся выражениями, должны быть заключены в круглые скобки. - person A. Levy; 18.08.2009
comment
да, я думаю, что ваш код должен быть if x > 1 then aux (x -1) (accum + x) - person a_m0d; 18.08.2009
comment
но +1 за то, что показал, как преобразовать в хвостовую рекурсивную функцию (теперь это действительно сносит мне голову - функциональное программирование достаточно сложно, чтобы понять, как я это написал) - person a_m0d; 18.08.2009
comment
поэтому, если я правильно понимаю, чтобы использовать хвостовые вызовы, функция должна быть единственной операцией (не разрешены операции с результатом вызова и т. д.) - person a_m0d; 18.08.2009
comment
@ a_m0d, tx для исправления синтаксиса, собираюсь отредактировать, чтобы исправить (как я уже сказал, я ржавый в синтаксисе O'Caml - например, как вызвать функцию с двумя аргументами - но ключевые идеи функционального программирования сожжены - поглубже ;-). - person Alex Martelli; 18.08.2009
comment
@ a_m0d снова, да, по сути: хвостовые вызовы можно оптимизировать тогда и только тогда, когда они ЯВЛЯЮТСЯ хвостом, то есть последней операцией в вызывающем; это часто, хотя и не всегда!, можно получить с помощью вспомогательной функции с аргументами, подобными накопителю. - person Alex Martelli; 18.08.2009

Функциональные языки обычно имеют НАМНОГО больший стек. Например, я написал функцию специально для проверки ограничений стека в OCaml, и она выполнила более 10 000 вызовов, прежде чем ее вырвало. Тем не менее, ваша точка зрения верна. Переполнения стека по-прежнему нужно остерегаться в функциональных языках.

Одной из стратегий, которые функциональные языки используют для снижения своей зависимости от рекурсии, является использование оптимизации хвостового вызова. Если вызов следующей рекурсии текущей функции является последним оператором в функции, текущий вызов может быть удален из стека, а на его месте создан новый вызов. Сгенерированные ассемблерные инструкции будут в основном такими же, как и для циклов while в императивном стиле.

Ваша функция не оптимизируется с помощью хвостового вызова, потому что рекурсия — не последний шаг. Он должен сначала вернуться, а затем он может добавить x к результату. Обычно это легко обойти, вы просто создаете вспомогательную функцию, которая передает аккумулятор вместе с другими параметрами.

let rec sum x =
  let sum_aux accum x =
    if x > 1 then sum_aux (accum + x) (x - 1)
    else x
  in sum_aux 0 x;;
person A. Levy    schedule 18.08.2009
comment
Функциональные языки не имеют больших стеков. Размер стека (для исполняемых файлов собственного кода) определяется ОС. OCaml может выполнять больше рекурсивных вызовов, вероятно, из-за ABI - параметры по умолчанию передаются в регистрах, поэтому используется меньше места в стеке. Я считаю, что вы можете добиться того же в C с соглашением о вызовах fastcall. - person ygrek; 02.02.2010
comment
Спасибо за исправление! Я думаю, что некоторые версии Windows (с которыми я столкнулся несколько лет назад) требуют/позволяют указать предполагаемый размер стека во время компоновки. Существует разумное значение по умолчанию, но если ваша программа использует много рекурсии или имеет функции с большими кадрами стека, вам нужно запросить больший стек. Наверное, я просто предположил, что так все и работает, и что OCaml должен быть настроен на запрос больших стеков по умолчанию, но, по-видимому, более хорошие операционные системы не имеют этого ограничения. До сих пор я не думал пересматривать, как работают стеки вызовов. Спасибо! - person A. Levy; 02.02.2010

Некоторые функциональные языки, такие как Scheme, указывают, что хвостовая рекурсия должна быть оптимизирована. быть эквивалентным итерации; следовательно, функция хвостовой рекурсии в Scheme никогда не приведет к переполнению стека, независимо от того, сколько раз она рекурсивна (предполагая, конечно, что она также не рекурсивно или не участвует во взаимной рекурсии в других местах, кроме конца).

Большинство других функциональных языков не требуют эффективной реализации хвостовой рекурсии; некоторые предпочитают это делать, другие нет, но это относительно легко реализовать, поэтому я ожидаю, что большинство реализаций делают это.

person Adam Rosenfield    schedule 18.08.2009
comment
Я полагаю, что это верно и для F#, так как изучение сгенерированного IL покажет, что создается обычный цикл. См. thevalerios.net/matt/ 2009/01/ - person Robert Harvey; 18.08.2009
comment
Некоторые из них могут потребовать, чтобы вы написали x + f(x - 1) вместо f(x - 1) + x, чтобы это считалось хвостовой рекурсией, верно? - person ShreevatsaR; 18.08.2009
comment
Это все равно не было бы хвостовой рекурсией, если бы было написано так. - person Thorarin; 18.08.2009
comment
@RobertHarvey: Да, компилятор F# превращает рекурсию хвоста в цикл, и, кроме того, вызовы хвоста выдают код операции IL .tail, который выполняет совокупную стоимость владения. (Примечание: в режиме отладки хвостовые вызовы отключены по умолчанию, чтобы сохранить стеки для отладки; см. флаг компилятора --tailcalls.) - person Brian; 19.08.2009

Новичкам, безусловно, легко писать глубокие рекурсии, которые взрывают стек. Objective Caml необычен тем, что функции библиотеки List не безопасны для стека для длинных списков. Такие приложения, как Unison, фактически заменили стандартную библиотеку Caml List библиотекой, безопасной для стека. версия. Большинство других реализаций лучше справляются со стеком. (Отказ от ответственности: моя информация описывает Objective Caml 3.08; текущая версия 3.11 может быть лучше.)

Стандартный ML штата Нью-Джерси отличается тем, что не использует стек, поэтому ваши глубокие рекурсии продолжаются. пока у вас не кончится куча. Это описано в отличной книге Эндрю Аппеля Компиляция с продолжением .

Я не думаю, что здесь есть серьезная проблема; это скорее «точка осознания» того, что если вы собираетесь писать много рекурсивного кода, что вы, скорее всего, будете делать на функциональном языке, вы должны знать о не-хвостовых вызовах и размере стека как по сравнению с размером данных, которые вы будете обрабатывать.

person Norman Ramsey    schedule 18.08.2009

Это сложно — в принципе да, но компиляторы и среды выполнения для функциональных языков учитывают повышенную степень рекурсии в функциональных языках. Самый простой из них заключается в том, что большинство исполняющих сред функциональных языков требуют гораздо большего стека, чем могли бы использовать обычные итеративные программы. Но в дополнение к этому компилятор функционального языка гораздо лучше способен преобразовать рекурсивный код в нерекурсивный из-за гораздо более строгих ограничений языка.

person olliej    schedule 18.08.2009