
- •Снова поиск с возвратом
- •Листинг 13.3. Программа сh06e03.Рго
- •Преимущества рекурсии
- •Оптимизация хвостовой рекурсии
- •Как задать хвостовую рекурсию
- •Листинг 13.5. Программа ch06e05.Pro
- •Листинг 13.6. Программа ch06e06.Pro
- •Использование аргументов в качестве переменных цикла
- •Листинг 13.7. Программа ch06e07.Pro
Листинг 13.6. Программа ch06e06.Pro
/* Показывает, как bedcount2 и bedcount3 могут быть улучшены объявлением отсечения ("cut") для исключения непроверенных предложений. Эти версии используют оптимизированную хвостовую рекурсию. */
predicates
cutcount2(long)
cutcount3(long)
check(long)
clauses
cutcount2(X):-
X>=0,
!,
write ('\r',X),
NewX = X + 1,
cutcount2(NewX).
cutcount2(_):-
write("X отрицательно.").
cutcount3(X):-write('\r',X), NewX = X+l, check(NewX), !, cutcount3(NewX).
check(Z):-Z >= 0.
check(Z):-Z < 0.
К сожалению, отсечение не сможет помочь с badcount1, в котором необходимость создания копий стековых фреймов не связана с непроверенными альтернативами. Единственный способ усовершенствовать badcount1 – произвести вычисления таким образом, чтобы рекурсивный вызов происходил в конце предложения.
Использование аргументов в качестве переменных цикла
Сейчас, после освоения хвостовой рекурсии, как бы вы поступили с циклическими переменными и счетчиками? Чтобы ответить на этот вопрос, мы совершим небольшое преобразование с Pascal на Пролог, предполагая, что вы знакомы с языком Pascal. Обычно результаты прямых переводов между двумя языками, как естественными, так и языками программирования, достаточно убоги. И хотя приведенный ниже пример неплох и является разумной иллюстрацией чисто процедурного программирования на Прологе, вам никогда не следует писать программы на Visual Prolog методом слепого перевода их с другого языка. Пролог – очень мощный и выразительный язык, и правильно написанные Пролог-программы показывают иной стиль программирования и имеют совсем иные проблемы, нежели программы на других языках.
В разд. "Рекурсивные процедуры" данной главы мы показали вычисление факториала с помощью рекурсивной процедуры. Здесь мы используем для этого итерацию. В Pascal это выглядело бы так:
Р := 1;
for I := 1 to N do P := P*I;
FactN := Р;
Если вы знакомы с Pascal, то знаете, что := является оператором присваивания и произносится как "присвоить". Здесь 4 переменных. N – число, факториал которого будет вычисляться; FactN – результат вычисления; I – циклическая переменная, изменяемая от 1 до N; Р – суммирующая переменная. Конечно, опытный программист на Pascal объединил бы FactN и Р, но для перевода на Пролог так будет удобнее.
Первый шаг в переводе на Пролог – замена for более простой формулировкой для цикла, точнее определяющей, что происходит с I на каждом шаге. Используем для этого определение while:
Р := 1; /* Инициализация Р и I */
I := 1;
while I <= N do /* Задание цикла */
begin
Р := Р*I; /* Обновление Р и I */
I := I+1;
end; FactN := Р; /* Показать результат */
Программа ch06e07.pro (листинг 117) показывает переведенный на Пролог цикл while языка Pascal.
Листинг 13.7. Программа ch06e07.Pro
predicates
factorial (unsigned, long)
factorial_aux(unsigned,lorn,unsigned, long)
% Числа, которые вероятно станут большими, объявляются long.
clauses
factorial(N, FactN):-
factonal_aux(N, FactN, 1, 1) .
factonal_aux(N, FactN, I, P) :-
I <= N, !,
NewP = P * I,
NewI =I+1,
factorial_aux(N, FactN, NewI, NewP).
factorial_aux(N, FactN, I, P) :-
I > N,
FactN = P.
Рассмотрим программу более детально.
У предложения для предиката factorial есть только два аргумента — N и FactN. Они являются как бы входом и выходом, если смотреть с точки зрения того, кто вычисляет факториал. Предложения для factorial_aux(N, FactN, I, P) фактически обеспечивают рекурсию. Их аргументами являются четыре переменные, которые должны передаваться из одного шага в другой. Поэтому factorial просто вызывает factorial_aux, передавая ему N и FactN с начальными значениями для I и Р:
factorial(N, FactN) :-
factorial_aux(N, FactN, 1, 1).
Так I и Р инициализируются.
Но как factorial передает FactN, ведь у нее пока нет еще значения? Ответ заключается в том, что концептуально Visual Prolog здесь унифицирует переменную, названную FactN в одном предложении, с переменной, названной FactN в другом предложении. Таким же образом factorial_aux передает себе FactN в качестве аргумента в рекурсивном вызове. В конечном счете последняя FactN получит значение, и после этого все другие FactN, которые унифицировались с ней, получат такое же значение. Мы сказали "концептуально", т. к. реально есть лишь одна FactN. Visual Prolog может определить из исходного кода, что FactN в действительности не используется перед вторым предложением factonal_aux, а все время передается одна и та же FactN.
Теперь о работе factonal_aux. Обычно этот предикат проверяет предложение " I меньше либо равно N" для циклического вычисления, а затем рекурсивно вызывает себя с новыми значениями для I и P. Здесь проявляется еще одна особенности Visual Prolog. В Прологе верное для арифметики выражение
Р = Р +1
совсем не является определением присвоения (как это должно быть на большинстве других языков программирования).
Замечание. Вы не можете изменить значение переменной в Visual Prolog.
В Прологе это так же абсурдно, как и в алгебре. Вместо этого вы должны создать новую переменную и придать ей нужное значение. Например:
NewP = Р + 1
Поэтому первое предложение выглядит следующим образом:
factonal_aux(N, FactN, I, P) :-
I <= N,
NewP = P*I, NewI = I+1,
factorial_aux(N, FactN, NewI, NewP).
Как и в случае cutcount2, в этом предложении отсечение будет обеспечивать оптимизацию хвостовой рекурсии, хотя оно и не является последним предложением в предикате.
В конечном счете I будет превышать N; текущие значения Р и FactN унифицируются и рекурсия прекратится. Это реализуется во втором предложении, которое выполнится, когда проверка I <= N в первом предложении будет неуспешна.
factonal_aux(N, FactN, I, P) :-
I > N,
FactN = P.
Здесь нет необходимости делать FactN = Р отдельным шагом; унификация может происходить в списке аргументов. Подстановка одинакового названия переменных требует, чтобы аргументы в этих позициях были равны. Более того, проверка I>N избыточна, т. к. обратное было проверено в первом предложении. Это дает завершающее предложение:
factorial_aux(_, FactN, _, FactN).
Упражнения
1. Приведенная в листинге 13.8 программа ch06e08.pro — улучшенная версия вычисления факториала.
Листинг 13.7. Программа ch06e07.pro
predicates
factorial (unsigned, long)
factorial (unsigned, long, unsigned, long)
clauses
factorial (N,FactN) :-
factorial(N,FactN, l, l) .
factorial (N, FactN, N, FactN) :- !.
factorial(N, FactN, I, P) :-
NewI = I+1,
NewP = P*NewI,
factorial (N, FactN, NewI, NewP).
Загрузите и выполните эту программу. Внимательно посмотрите на код второго предложения factorial/4. Оно использует преимущество того факта, что во время первого его вызова переменная-счетчик I всегда равна 1. Это позволяет выполнять шаг умножения вместе с увеличенной переменной-счетчиком NewI, а не с 1, экономя тем самым одну рекурсию/итерацию. Это отражено в первом предложении.
2. Напишите программу с хвостовой рекурсией, которая будет работать как программа ch06e08.pro, но без поиска с возвратом.
3. Напишите программу с хвостовой рекурсией, которая печатает таблицу степеней числа 2, как показано ниже:
N 2^N
_______
1 2
2 4
3 8
4 16
…..
10 1024
Остановите программу при N = 10.
4. Напишите программу с хвостовой рекурсией, которая допускает ввод числа и способна завершаться двумя способами. Она должна начинаться умножением числа на себя до тех пор, пока не достигнет числа 81 или числа, большего чем 100. Если достигнуто число 81, то печатается "yes", если же число больше 100 – печатается "nо".