Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ЛАБОРАТОРНАЯ РАБОТА 7.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
150.02 Кб
Скачать

Как задать хвостовую рекурсию

Что означает фраза "одна процедура вызывает другую, выполняя свой самый последний шаг"? На языке Пролог это значит:

  • вызов является самой последней подцелью предложения;

  • ранее в предложении не было точек возврата.

Ниже приводится удовлетворяющий обоим условиям пример:

count(N) :-

write(N), nl,

NewN = N+l,

count(NewN).

Эта процедура является хвостовой рекурсией, которая вызывает себя без резервиро­вания нового стекового фрейма, и поэтому не истощает запас памяти. Как показы­вает программа ch06e04.pro (листинг 13.4), если вы дадите ей целевое утверждение

count(0).

то предикат count будет печатать целые числа, начиная с 0, и никогда не остановит­ся. В конечном счете произойдет целочисленное переполнение, но остановки из-за истощения памяти не произойдет.

Листинг 13.4. Программа ch06e04.pro

/* Программа с хвостовой рекурсией, которая не истощает память */

Predicates

count(ulong)

clauses

count(N):-

write('\r', N),

NewN = N+l,

count(NewN).

goal

nl, count(0) .

Упражнение

Преобразуйте программу ch06e04.pro так, чтобы не было больше хвостовой рекур­сии. Сколько итераций может она выполнить до истощения своей памяти? Попробуйте и посмотрите. (На 32-битных платформах это займет заметное время, и программа, вероятно, не превысит виртуального стекового адресного пространства. Более вероятно, что операционная система израсходует доступную физическую память. На 16-битных платформах число возможных итераций напрямую зависит от размера стека задачи.)

Из-за чего возникает не оптимизированная хвостовая рекурсия

Выше был продемонстрирован правильный пример использования хвостовой рекур­сии, программа ch06e05.pro показывает три ошибочных способа организации хво­стовой рекурсии.

  • Если рекурсивный вызов — не самый последний шаг, процедура не является хво­стовой рекурсией. Например:

Badcount1(X) :-

write('\r',X),

NewX = X+1,

badcountl(NewX),

nl.

Каждый раз, когда badcount1 вызывает себя, стек должен быть сохранен для того, чтобы обработку можно было вернуть к вызывающей процедуре, которая должна выполняться до nl. Поэтому она сделает всего несколько тысяч рекурсивных вы­зовов до исчерпания памяти.

  • Другой способ сделать хвостовую рекурсию не оптимизированной – оставить некоторую возможную альтернативу непроверенной к моменту выполнения ре­курсивного вызова. Тогда стек должен быть сохранен, т. к. в случае неудачного завершения рекурсивного вызова вызывающая процедура может откатиться и на­чать проверять эту альтернативу. Например:

badcount2(X):-

write ('\r', X),

NewX = Х+1,

badcount2(NewX).

badcount2(X):-

X < 0,

write("X отрицательно.").

Здесь первое предложение badcount2 вызывает себя, когда второе предложение еще не выполнено. Снова программа истощает память после определенного ко­личества вызовов.

  • Для потери оптимизации хвостовой рекурсии не обязательно иметь непроверен­ную альтернативу как отдельное предложение рекурсивной процедуры. Непрове­ренная альтернатива может быть и в любом вызываемом предикате. Например:

badcount3(X) :-

write ('\r',X),

NewX = X+l,

check (NewX),

badcount3(NewX) .

check(Z) :- Z >= 0.

check(Z) :- Z < 0.

Предположим, что X – положительная величина, как это обычно бывает. Когда badcount3 вызывает себя, первое предложение check достигает цели, а второе предположение check еще не проверено. Поэтому badcount3 должен сохранить копию моего стекового фрейма, чтобы иметь возможность вернуться и начать проверять второе предложение check в случае, если рекурсивный вызов завершится неудачно (листинг 13.5).