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

Листинг 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 на Пролог, предполагая, что вы знакомы с языком Pas­cal. Обычно результаты прямых переводов между двумя языками, как естественны­ми, так и языками программирования, достаточно убоги. И хотя приведенный ниже пример неплох и является разумной иллюстрацией чисто процедурного программи­рования на Прологе, вам никогда не следует писать программы на 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о".