На этом шаге мы рассмотрим использование аргументов в качестве переменных цикла.
Сейчас, после освоения хвостовой рекурсии, как бы вы поступили с циклическими переменными и счетчиками? Чтобы ответить на этот вопрос, мы совершим небольшое преобразование с Pascal на Пролог, предполагая, что вы знакомы с языком Pascal. Обычно результаты прямых переводов между двумя языками, как естественными, так и языками программирования, достаточно убоги. И хотя приведенный ниже пример неплох и является разумной иллюстрацией чисто процедурного программирования на Прологе, вам никогда не следует писать программы на Пролог методом слепого перевода их с другого языка. Пролог - очень мощный и выразительный язык, и правильно написанные Пролог-программы показывают иной стиль программирования и имеют совсем иные проблемы, нежели программы на других языках.
На шаге 39 мы показали вычисление факториала с помощью рекурсивной процедуры. Здесь мы используем для этого итерацию. В Pascal это выглядело бы так:
Р:=1; for I:=1 to N do P:=P*I; FactN:=P;
Если вы знакомы с Pascal, то знаете, что := является оператором присваивания и произносится как "присвоить". Здесь 4 переменных. N - число, факториал которого будет вычисляться; FactN - результат вычисления; I - циклическая переменная, изменяемая от 1 до N; P - суммирующая переменная. Конечно, опытный программист на Pascal объединил бы FactN и Р, но для перевода на Пролог так будет удобнее.
Первый шаг в переводе на Пролог - замена for более простой формулировкой для цикла, точнее определяющей, что происходит с I на каждом шаге. Используем для этого определение while:
Р:=1; /* Инициализация Р и I */ I:=1; while I <= N do /* Задание цикла */ begin Р:=Р*I; /* Обновление Р и I */ I:=I+1 end; FactN:= P; /* Показать результат */
predicates factorial(integer,integer) factorial_aux(integer,integer,integer,integer) clauses factorial(N, FactN):- factorial_aux(N,FactN,1,1). factorial_aux(N,FactN,I,P):- I <= N,!, NewP=P * I, New1=I+1, factorial_aux(N,FactN,New1,NewP). factorial_aux(N,FactN,I,P):- I > N, FactN=P.
Результат работы программы можно посмотреть на рис.1
Рис.1. Результат работы программы pro41_1.pro
Рассмотрим программу более детально. У предложения для предиката factorial есть только два аргумента - N и FactN. Oни являются как бы входом и выходом, если смотреть с точки зрения того, кто вычисляет факториал. Предложения для factorial_aux(N,FactN,I,P) фактически обесчивают рекурсию. Их аргументами являются четыре переменные, которые должны предаваться из одного шага в другой. Поэтому factorial просто вызывает factorial_aux, передавая ему N и FactN с начальными значениями для I и Р:
factorial(N,FactN):- factorial_aux(N,FactN,1,1).
Так I и P инициализируются.
Но как factorial передает FactN, ведь у нее пока нет еще значения? Ответ заключается в том, что Пролог здесь унифицирует переменную, названную FactN в одном предложении, с переменной, названной FactN в другом предложении. Таким же образом factorial_aux передает себе FactN в качестве аргумента и рекурсивном вызове. В конечном счете последняя FactN получит значение, и после этого все другие FactN, которые унифицировались с ней, получат такое же значение. Пролог может определить из исходного кода, что FactN в действительности не используется перед вторым предложением factorial_aux, а все время передается одна и та же FactN.
Теперь о работе factorial_aux. Обычно этот предикат проверяет предложение "I меньше либо равно N" для циклического вычисления, а затем рекурсивно вызывает себя с новыми значениями для I и P. Здесь проявляется еще одна особенность Пролога. В Прологе верное для арифметики выражение
P=Р+1
В Прологе это так же абсурдно, как и в алгебре. Вместо этого вы должны создать новую переменную и придать ей нужное значение. Например:
NewP=Р+1
factorial_aux(N,FactN,I,P):- I <= N,!, NewP=P*I, Newl=I+1, factorial_aux(N,FactN,Newl,NewP).
Как и в случае cutcount2, в этом предложении отсечение будет обеспечивать оптимизацию хвостовой рекурсии, хотя оно и не является последним предложением в предикате.
В конечном счете I будет превышать N; текущие значения P и FactN унифицируются и рекурсия прекратится. Это реализуется во втором предложении, которое выполнится, когда проверка I<= N в первом предложении будет неуспешна.
factorial_aux(N,FactN,I,P):- I>N, FactN=P.
Здесь нет необходимости делать FactN=P отдельным шагом; унификация может происходить в списке аргументов. Подстановка одинакового названия переменных требует, чтобы аргументы в этих позициях были равны. Более того, проверка I>N избыточна, т. к. обратное было проверено в первом предложении. Это дает завершающее предложение:
factorial_aux(_,FactN,_,FactN).
На следующем шаге мы начнем рассматривать списки и рекурсию.