
- •Снова поиск с возвратом
- •Листинг 13.3. Программа сh06e03.Рго
- •Преимущества рекурсии
- •Оптимизация хвостовой рекурсии
- •Как задать хвостовую рекурсию
- •Листинг 13.5. Программа ch06e05.Pro
- •Листинг 13.6. Программа ch06e06.Pro
- •Использование аргументов в качестве переменных цикла
- •Листинг 13.7. Программа ch06e07.Pro
Как задать хвостовую рекурсию
Что означает фраза "одна процедура вызывает другую, выполняя свой самый последний шаг"? На языке Пролог это значит:
вызов является самой последней подцелью предложения;
ранее в предложении не было точек возврата.
Ниже приводится удовлетворяющий обоим условиям пример:
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).