LINUX.ORG.RU

О хвостовой рекурсии

 


3

4

Или с чем ее едят и как ее готовят? Набрел на статью в Википедии, ознакомился, сделал сравнительный замер времени выполнения для приведенного там примера. Результаты впечатлили, особенно если учесть, что тестировал я это для Scala с включенной оптимизацией.пиляторы

Собственно вопросов два.

1. Хотелось бы получше разобраться в устройстве хвосто-рекурсивных функций и их вызовов, дабы объяснить человеку, далекому от этой темы - в чем там магия?

2. Какие еще популярные ЯП и компиляторы поддерживают оптимизацию данного вида рекурсии?

Всем спасибо.

1. Хотелось бы получше разобраться в устройстве хвосто-рекурсивных функций и их вызовов, дабы объяснить человеку, далекому от этой темы - в чем там магия?

http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-2.html#%_toc_%_sec_3.5

2. Какие еще популярные ЯП и компиляторы поддерживают оптимизацию данного вида рекурсии?

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

encyrtid ★★★★★
()

Кстати, Scala такую оптимизацию в полной мере не поддерживает, но это уже ограничения JVM.

dave ★★★★★
()

Какие еще популярные ЯП и компиляторы поддерживают оптимизацию данного вида рекурсии?

Scheme, некоторые реализации CL (SBCL), Haskell. А еще посоны говорили, что gcc умеет TCO, поэтому Си тоже можно добавить в список :)

yoghurt ★★★★★
()

Боже мой, ну какая магия? Просто в текущем стэк фрэйме заменяются actual parameters на новые и вычисления продолжаются без создания нового фрэйма. По сути вместо рекурсивной функции получился цикл.

nokachi
()
Ответ на: комментарий от yoghurt

А еще посоны говорили, что gcc умеет TCO, поэтому Си тоже можно добавить в список :)

Опяит таки с ограничениями, variadic functions не совместимы с TCO. А так в список ещё надо добавить OCaml, SML, F# и прочие диалекты ML, Tcl 8.6.

Begemoth ★★★★★
()
Последнее исправление: Begemoth (всего исправлений: 1)

tail call — если функция последним действием вызывает функцию, возвращая её результат, то можно заменить это последнее действие на некий аналог goto в начало другой функции. Собственно, вместо call-вычисления-return-return получается goto-вычисления-return и можно забыть обо всём, что было в оригинальной функции.

tail recursion — частный случай, когда функция последним действием вызывает себя.

x3al ★★★★★
()
Ответ на: комментарий от x3al

Tail call может быть рекурсивным (while в F# Async). Что-то неладно в этой терминологии.

dave ★★★★★
()
Последнее исправление: dave (всего исправлений: 1)
Ответ на: комментарий от x3al

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

dave ★★★★★
()
Ответ на: комментарий от dave

tail recursion — частный случай tail call, когда call идёт на себя. В этом вашем F# фактически описывается функция, не?

x3al ★★★★★
()

ХР - это костыль для недоязычкой не умеющих циклы.

А эстетический оргазм от наблюдения ажурных костылей - извращение.

Deleted
()
Ответ на: комментарий от x3al

Твое определение является проблемным безотносительно F#, исходя из общих рассуждений. А, вообще, вот та функция:

        /// Implement the while loop
        let rec whileA gd prog =
            if gd() then 
                bindA prog (fun () -> whileA gd prog) 
            else 
                doneA

Нет, я понимаю, кому-то очень хотелось выделить тот случай, когда хвостовая рекурсия сводится к циклу, и красиво это назвать, но только этот случай называть хвостовой рекурсией как-то некорректно на мой взгляд - искажается сам смысл понятия рекурсии. Ну да, ладно. Определение, видимо, не твое.

dave ★★★★★
()
Ответ на: комментарий от x3al

Еще раз.

1. Функция whileA определена рекурсивно, о чем намекает ключевое слово «rec».

2. Везде используются хвостовые вызовы (не съедается стек).

3. Хвостовой вызов в whileA, использующий рекурсию, не подподает под «твое» определение «хвостовой рекурсии».

4. Явно что-то здесь не так.

dave ★★★★★
()
Ответ на: комментарий от dave

whileA не содержит хвостового вызова whileA, с-но хвостовой рекурсии тут нету. С определением всё в порядке.

anonymous
()
Ответ на: комментарий от dave

С какой такой позицией? Что не надо искать магию там где ее нет? Пожалуй, да.

nokachi
()

Видел вот эту статью? Там собраны описания и примеры всяких разных элементов функциональных языков. По сабжу см. п. 7 — Хвостовой вызов.

ymn ★★★★★
()

Awwww, Java-кодерок делает первые шажки, как это мило.

anonymous
()
Ответ на: комментарий от dave

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

Эта фигня идёт от английских определений, где ни кто практически не говорит «tail recursion», но все говорят «tail call». В русском наоборот - «хвостовой вызов» как-то не употребляется. И да, «хвостовая рекурсия» - частный случай «хвостового вызова», но полноценная поддержка последнего - довольно серьёзная «фича», встречающаяся довольно редко. А вот «хвостовую рекурсию» именно через «банальный цикл» (в виде «банального GOTO») поддерживают уже почти все кому не лень.

Не?

yyk ★★★★★
()
Ответ на: комментарий от anonymous

whileA не содержит хвостового вызова whileA, с-но хвостовой рекурсии тут нету. С определением всё в порядке.

Именно, что содержит хвостовые вызовы. Там все вызовы - хвостовые. Это одна из фишек продолжений, на которых основана монада Async в F#.

dave ★★★★★
()
Ответ на: комментарий от yyk

Я тоже думаю, что трудности перевода. Например, буквально позавчера встретил такое выражение в RWH: «tail-recursive function». Как я понимаю, «tail-recursive» больше относится к функциям, тогда как «tail call» относится к самому вызову.

dave ★★★★★
()
Ответ на: комментарий от yoghurt

А еще посоны говорили, что gcc умеет TCO, поэтому Си тоже можно добавить в список :)

вериться с трудом - gcc вообще не любит функции раскрывать, а уж рекурсивные...

drBatty ★★
()

Хвостовая рекурсия - дрянь, и приличный компилятор не имеет права применять эту копеечную, бесполезную оптимизацию по умолчанию. Соответственно, никакой кодеришко не имеет права рассчитывать на то, что компилятор эту «оптимизацию» говняную сделает.

А почему ХР дрянь, спросите вы? Ну и тупые. Элементарно же - после такой «оптимизации» хрена огрызок вы увидите в stack trace, вместо того, что должно быть на самом деле. Соответственно, дебажить такой говнокод становится просто невозможно.

В день, когда в JVM появится вдруг поддержка хвостовой рекурсии (чур меня, чур, надеюсь, такого не будет никогда), мне придется переходить на какую-либо другую технологию. Может, на C++ вернусь.

anonymous
()
Ответ на: комментарий от dave

Именно, что содержит хвостовые вызовы.

Конечно, содержит. Любая функция содержит хвостовые вызовы (т.к. последний вызов - всегда хвостовой). Но хвостовых вызовов whileA она не содержит. Она _вообще_ не содержит _никаких_ вызовов whileA. Там неявная рекурсия. Сперва идет хвостовой вызов bindA, потом хвостовой вызов санки (я так полагаю, т.к. реализации bindA не знаю) и, наконец, в этой санке хвостовой вызов whileA.

anonymous
()
Ответ на: комментарий от anonymous

Ну, и что имеем? А имеем хвостовой вызов bindA, который рекурсивно вызывает whileA. И этот вызов не подпадает под определение «хвостовой рекурсии» выше. Это раз. Потом, сам рекурсивный вызов whileA является хвостовым (sic!) для whileA (да-да, продолжения), но поскольку он неявный, то он тоже не подпадает под то определение «хвостовой рекурсии». Это два. Нет, тут явно с терминологией что-то не так.

Ладно. Мне надо работать. Разбирайтесь тут сами со своей терминологией. Не удивлюсь, если на википедии есть похожие ляпы.

dave ★★★★★
()
Ответ на: комментарий от anonymous

Ты что, тупой? «Оптимизация» хвостовых вызовов СТИРАЕТ из стека НЕОБХОДИМУЮ для дебага информацию. А в случае с циклом все на месте, ничто никуда не исчезает.

anonymous
()
Ответ на: комментарий от anonymous

Любая функция содержит хвостовые вызовы (т.к. последний вызов - всегда хвостовой).

Тупой? Где хвостовые вызовы в функции:

int f(int x, int y) { return g(x) + g(y); }

anonymous
()
Ответ на: комментарий от anonymous

P.S. и в отличие от register allocation (ну, все помнят в дебаггере облом про «value optimized out») это имеет последствия далеко за пределами интерактивной отладки. Например, если где-то в коде швыряется exception, то его stack trace тоже будет полным враньем, если где-то по дороге были хвостовые говновызовы.

anonymous
()
Ответ на: комментарий от anonymous

Ты что, тупой? «Оптимизация» хвостовых вызовов СТИРАЕТ из стека НЕОБХОДИМУЮ для дебага информацию. А в случае с циклом все на месте, ничто никуда не исчезает.

мне кажется, или все нормальные люди если и берут в руки дебагер, то делают это с -O0 ?

Waterlaz ★★★★★
()
Ответ на: комментарий от Waterlaz

Тебе кажется. Потому как оптимизации могут сильно поменять поведение кода. Нормальные люди берут в руки дебагер с "-g -O2" как минимум.

Кроме того, смотри выше про exception-ы. Они-то как раз представляют особый интерес, когда в продакшене вылетают.

anonymous
()
Ответ на: комментарий от anonymous

Ты что, тупой? «Оптимизация» хвостовых вызовов СТИРАЕТ из стека НЕОБХОДИМУЮ для дебага информацию. А в случае с циклом все на месте, ничто никуда не исчезает.

вообще-то никакая информация никуда не девается. То, что в стеке - это ИЗБЫТОЧНАЯ информация, по логике алгоритма там её и быть не должно.

drBatty ★★
()
Ответ на: комментарий от drBatty

вообще-то никакая информация никуда не девается. То, что в стеке - это ИЗБЫТОЧНАЯ информация, по логике алгоритма там её и быть не должно.

Она не избыточная, она необходимая для анализа, так как содержит промежуточные результаты.

anonymous
()
Ответ на: комментарий от anonymous

Она не избыточная, она необходимая для анализа, так как содержит промежуточные результаты.

если у тебя цикл, то промежуточных результатов тоже нет. А стало быть экономится место и время (как не крути, но долбить аккумулятор быстрее, чем пихать результат в стек). Для дебага можешь и дополнительный массив за #ifdef заделать...

drBatty ★★
()
Ответ на: комментарий от drBatty

Если у меня цикл, то я могу breakpoint хотя бы разумно поставить и повторить. А если что-то не так в коде, где «оптимизировали» хвостовые вызовы, то я даже знать не буду, где breakpoint ставить, вся история потеряна.

anonymous
()

2. Какие еще популярные ЯП и компиляторы поддерживают оптимизацию данного вида рекурсии?

gcc, например

qnikst ★★★★★
()
Ответ на: комментарий от anonymous

Если у меня цикл, то я могу breakpoint хотя бы разумно поставить и повторить.

дык не юзай хвостовые вызовы - делов-то? ну или volatile пригодится, если так хочется отключить такое поведение.

drBatty ★★
()
Ответ на: комментарий от drBatty

дык не юзай хвостовые вызовы - делов-то? ну или volatile пригодится, если так хочется отключить такое поведение.

Что значит «не юзай»? Тупой, что ли? Переписывать код только из-за того, что компилятор писали недоумки? Нет уж, я лучше просто перейду на компилятор, который этого говна ГАРАНТИРОВАННО не делает. Потому что «такое поведение» отключать надо всегда. Пользы от него ноль, один вред.

anonymous
()
Ответ на: комментарий от qnikst

gcc, например

кстати да

void f(const char *s)
{
	if(!*s)
		return;
	printf("%c", *s);
	f(++s);
}
08048440 <f>:
 8048440:	53                   	push   ebx
 8048441:	83 ec 18             	sub    esp,0x18
 8048444:	8b 5c 24 20          	mov    ebx,DWORD PTR [esp+0x20]
 8048448:	0f be 03             	movsx  eax,BYTE PTR [ebx]
 804844b:	84 c0                	test   al,al
 804844d:	74 11                	je     8048460 <f+0x20>
 804844f:	90                   	nop
 8048450:	89 04 24             	mov    DWORD PTR [esp],eax
 8048453:	e8 88 fe ff ff       	call   80482e0 <putchar@plt>
 8048458:	43                   	inc    ebx
 8048459:	0f be 03             	movsx  eax,BYTE PTR [ebx]
 804845c:	84 c0                	test   al,al
 804845e:	75 f0                	jne    8048450 <f+0x10>
 8048460:	83 c4 18             	add    esp,0x18
 8048463:	5b                   	pop    ebx
 8048464:	c3                   	ret    

drBatty ★★
()
Ответ на: комментарий от anonymous

Что значит «не юзай»? Тупой, что ли? Переписывать код только из-за того, что компилятор писали недоумки?

может дело не в бобине, а просто йух сидит в кабине? См. код в моём прошлом сообщении - сколько надо было выпить ъаду, что-бы такое написать IRL?

drBatty ★★
()
Ответ на: комментарий от anonymous

Тебе кажется. Потому как оптимизации могут сильно поменять поведение кода. Нормальные люди берут в руки дебагер с "-g -O2" как минимум.

ты просто не видел нормальных людей

Кроме того, смотри выше про exception-ы. Они-то как раз представляют особый интерес, когда в продакшене вылетают.

Проблемы говноязыков какие-то.

Waterlaz ★★★★★
()
Ответ на: комментарий от drBatty

много теории о том как правильно реализовывать tail-recursion, и как правильно писать код, чтобы его в tail-recursion можно было завернуть, рассмотрены разные случаи и приведена реализация (как со стороны пишущего код, так и во что это скомпилируется) в gcc, ну и рассмотрены особенности разных архитектур в применении к ХР.

qnikst ★★★★★
()
Последнее исправление: qnikst (всего исправлений: 1)
Ответ на: комментарий от Waterlaz

ты просто не видел нормальных людей

Ты вообще программист, или так, студентота? Не похож ты на программиста, если честно.

anonymous
()
Ответ на: комментарий от anonymous

Ты вообще программист, или так, студентота? Не похож ты на программиста, если честно.

если честно, но в open source дебагер попросту не нужен... Не, это действительно так...

drBatty ★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.