LINUX.ORG.RU

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

 


3

4

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

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

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

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

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

Представь обычный while. У тебя есть набор переменных, которые изменяются при итерации или просто используются внутри цикла. Пишешь функцию и их все описываешь в виде параметров. В конце функции вызываешь ее же саму с новыми значениями всех параметров. В конце функции ставишь if с условием начального цикла, если false - выходишь. Аналогично функция с хвостовой рекурсией превращается назад в while. Тоесть они взаимозаменяемы и переводятся одна в другую вполне формально.

Но ясное дело что как бы не написали в коде, реализация через обычный while будет оптимальнее выполняться. Вот ее и выбирает компилятор.

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

много теории о том как правильно реализовывать tail-recursion, и как правильно писать код, чтобы его в tail-recursion можно было завернуть

а можно в двух словах? Лично мне непонятно, ЗАЧЕМ? Вот код который выше - тупой, и он сворачивается. Лично я не понимаю, как его сделать хоть на что-то годным? Вот так например:

void f(const char *s)
{
	if(!*s)
		return;
	char c = *s++;
	f(s);
	printf("%c", c);
}
этот код печатает строку в обратном порядке. Но это не хвостовая рекурсия, и она НЕ сворачивается в цикл.

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

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

Если серьезно, то языки в которые не добавили циклы по умолчанию часто основаны на лямбда исчислении, достаточно мощной абстракции. Лямбда исчисление не сильно нужно непосредственно в программировании, но оно достаточно минимально чтобы математики могли исследовать свойства любых алгоритмов рассматривая только малую часть самых низкоуровневых операций, тоесть только то что в лямбда исчислении. То же самое что исследовать свойства алгоритмов на базе машины Тьюринга, потому что доказанное будет справедливо для всех ЯП. Но машина Тьюринга уже слишком миниатюрна, потому неудобно.

vertexua ★★★★★
()
Последнее исправление: vertexua (всего исправлений: 2)
Ответ на: комментарий от drBatty

а можно в двух словах?

нет, прочитай хотя бы abstract

Лично мне непонятно, ЗАЧЕМ?

ЗАЧЕМ ЧТО?

Лично я не понимаю, как его сделать хоть на что-то годным?

и?

Но это не хвостовая рекурсия, и она НЕ сворачивается в цикл.

и что дальше то? что ты этим доказал?

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

Ну ясно, студентота. Давай, до свидания!

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

Почему?

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

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

Да, и у меня опечатка, надо «в начале функции ставишь if»

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

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

В догонку:

Henry G. Baker. Garbage in/garbage out: When bad programs happen to good people. ACM SIGPLAN Notices, 32(3):27{31} March 1997.

M. Anton Ertl. Stack caching for interpreters. In SIGPLAN ’95 Conference on Programming Language Design and Implementa- tion, pages 315{327}, 1995

так же github://hopc пример, где это явно использовалось.

qnikst ★★★★★
()

1. Дык гугл же.
2. F# например.

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

является хвостовым (sic!)

Я что-то не понял, кого именно ты тут цитируешь, чтобы sic-ать?

Переводи с латыни: «именно так!»

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

В математике всё, что не тривиально, - ошибочно.

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

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

в гриме не видно, да

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

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

«The most effective debugging tool is still careful thought, coupled with judiciously placed print statements.»

Честно, дебагер стараюсь по возможности не трогать.

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

А имеем хвостовой вызов bindA, который рекурсивно вызывает whileA.

Вызов whileA в bindA не рекурсивный.

Потом, сам рекурсивный вызов whileA является хвостовым (sic!) для whileA

Хвостовым вызовом для whileA является вызов bindA. Вызов whileA вообще для whileA никаким вызовом не является - ни хвостовым, ни нехвостовым, т.к. этого вызова там банально нет.

И этот вызов не подпадает под определение «хвостовой рекурсии» выше.

Вызов bindA в whileA - хвостовой. Вызов санки в bindA - хвостовой. Вызов whileA в санке - хвостовой. Что там у вас не подпадает и почему должно подпадать?

но поскольку он неявный, то он тоже не подпадает под то определение «хвостовой рекурсии»

Конечно, не подпадает. Он и не должен. С какой стати, собственно?

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

Например, если где-то в коде швыряется exception, то его stack trace тоже будет полным враньем, если где-то по дороге были хвостовые говновызовы.

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

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

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

В случае с циклом имеется _точно та же_ информация, что и в случае хвосто-рекурсивной реализации этого цикла. Так что либо информация стирается и циклом в том числе, либо никакой нужной информации не стирается при оптимизации TCO. Выбирай.

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

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

Точно так же можно поставить брекпоинт и с TCO.

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

В цикле тоже вся история потеряна, и ты не знаешь где поставить брекпоинт.

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

зачем ХР нужна.

Иногда она удобнее, чем цикл. Ну и кроме того речь не о ХР, а о TCO в общем. Спрашивать зачем нужна TCO, ну это то же самое, что спрашивать, зачем нужно RA или инлайнинг.

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

А чего со мной спорить? Есть общепринятые определения. Хвостовой вызов вообще формально строго определяется в семантике конкретного языка. Формулами, а не словами.

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

Ну, я вижу, что на счет хвостового вызова мы нашли общие точки соприкосновения, но это не помешало тебе о чем-то спорить.

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

Иногда она удобнее, чем цикл.

примеры кода будут? или так, попиз^Wпотрепаться?

Ну и кроме того речь не о ХР, а о TCO в общем. Спрашивать зачем нужна TCO, ну это то же самое, что спрашивать, зачем нужно RA или инлайнинг.

ага. ещё тоже самое, что спрашивать, зачем нужно 2*2. Ну и зачем?

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

В Си? +? Функцией? Юноша, вы дёбил?

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

Чушь и какашка. Еще один баран, путающий хвостовую рекурсию в частности с хвостовыми вызовами вообще. Не понимаешь, что вся цепочка вызовов может оказаться рекурсивной, и ты получишь stack trace из одного элемента, а как и почему ты туда попал - неизвестно?

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

Точно так же можно поставить брекпоинт и с TCO.

Нельзя. Идиот. В случае с циклом я знаю, каким путем исполнение туда пришло. В случае с «оптимизацие» хвостовых вызовов информация о control flow потеряна.

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

ага. ещё тоже самое, что спрашивать, зачем нужно 2*2. Ну и зачем?

Зачем нужны оптимизации? Ну чтобы программа работала быстрее и занимала меньше памяти. А ты не знал?

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

Еще один баран, путающий хвостовую рекурсию в частности с хвостовыми вызовами вообще.

я как раз говорил только о хвостовых вызовах.

Не понимаешь, что вся цепочка вызовов может оказаться рекурсивной

Если цепочка оказалась вдруг рекурсивной, то это был цикл. И стектрейса в этом случае быть не должно и не будет - так же как не должно и не будет его в случае цикла.

и ты получишь stack trace из одного элемента, а как и почему ты туда попал - неизвестно?

С циклом то же самое.

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

Нельзя. Идиот. В случае с циклом я знаю, каким путем исполнение туда пришло.

В случае с TCO и preserved stack тоже.

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

Какой цикл, придурок? Все функции в цкпочке могут быть разными.

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

А в каком месте исправлять искать с помощью 100500 printf'ов ?

ну если у тебя 100500 мест с былокодом - то да... Придётся так делать. Конечно давить 100500 раз F8 или что там у тебя next step - оно проще, надёжнее и безопаснее.

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

Я открою дамп в _отладчике_, посмотрю просмотрю стек и исправлю проблемное место. Ну а такой хелло-ворлдщик как ты может конечно развлекаться с расстановкой 100500 принтфов и 100500 перекомпиляций/перезапусков на каждый принтф.

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

Я открою дамп в _отладчике_, посмотрю просмотрю стек и исправлю проблемное место.

Ты же и сам знаешь, что так получается отнюдь не всегда :)

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

Зачем нужны оптимизации? Ну чтобы программа работала быстрее и занимала меньше памяти. А ты не знал?

нет. я не знал, не знаю, и похоже не узнаю, за кой хрен городить антиоптимизацию в надежде, что мой цикл завёрнутый в ХР компилятор развернёт обратно в цикл. Ты можешь сказать ЗАЧЕМ заворачивать цикл в ХР? Зачем его обратно разворачивать - мне понятно.

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

Я открою дамп в _отладчике_, посмотрю просмотрю стек и исправлю проблемное место.

ты откроешь дамп, и увидишь 100500 ОДИНАКОВЫХ точек входа. И на 100501й креш из-за переполнения стека. Моя программа тупо зависнет прокручивая цикл. Это хуже/лучше?

Ну а такой хелло-ворлдщик как ты может конечно развлекаться с расстановкой 100500 принтфов и 100500 перекомпиляций/перезапусков на каждый принтф.

передёргиваешь: у нас есть ОДИН цикл свёрнутый в ХР. Вопрос - нужно-ли что-бы компилятор его разворачивал? да/нет?

Мой ответ - ХР не нужна. Вообще. Ибо я не видел ни одного примера, где она помогает. Потому оптимизация не имеет смысла, как оптимизация деления на ноль.

ЗЫЖ и да, лучше 100500 принтфов в дебаге, чем засранный стек и в дебаге, и в продакшене. Хотя теоретикам типа тебя этого не понять похоже.

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

ты откроешь дамп, и увидишь 100500 ОДИНАКОВЫХ точек входа. И на 100501й креш из-за переполнения стека. Моя программа тупо зависнет прокручивая цикл. Это хуже/лучше?

ЩИТО? Это ты к чему?

передёргиваешь: у нас есть ОДИН цикл свёрнутый в ХР. Вопрос - нужно-ли что-бы компилятор его разворачивал? да/нет?

Я про отладчик, который по твоим словам «не нужен».

ЗЫЖ и да, лучше 100500 принтфов в дебаге, чем засранный стек и в дебаге, и в продакшене. Хотя теоретикам типа тебя этого не понять похоже.

Ты как всегда в свое репертуаре — позорно слил и в кусты/на другую тему.

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