Если оператор объявлен со спецификатором xfy, то он является правоассоциативным.
Таблица 6.1 – Операторы языка Пролог
Имя
Спецификатор
Приоритет
:-
xfx
1200
?-
fx
1200
;
xfy
1100
,
xfy
1000
.
xfy
750
=..
xfx
700
=
xfx
700
=\=
xfx
700
is
xfx
700
=:=
xfx
700
<
xfx
700
=<
xfx
700
>
xfx
700
>=
xfx
700
==
xfx
700
\==
xfx
700
–
yfx
500
+
yfx
500
/
yfx
400
*
yfx
400
div
yfx
400
mod
xfx
300
6.4. Арифметические выражения
Язык Пролог предназначен, главным образом, для программирования задач символьной обработки информации. Тем не менее, в любую пролог систему включаются все обычные арифметические операторы: + (сложение), - (вычитание), * (умножение), / (деление), mod (остаток от деления целых чисел), div (целочисленное деление).
ВПролог входят также все необходимые операторы сравнения чисел:
<(меньше), > (больше), >= (больше или равно), =< (меньше или равно), =\= (не равно), =:= (равенство целочисленных выражений).
Арифметические выражения в Прологе строят из арифметических операторов, числовых переменных, констант и скобок. При этом арифметические выражения представляют собой структуры. Например, выражение 1+2*3 представляется структурой, изображенной на рисунке 6.3. Если пролог-системе задать вопрос
Язык Пролог
321
? – Х=1+2*3.
то в результате получим Х=1+2*3, а не Х=7.
Рисунок 6.3 – Представление арифметического выражения 1+2*3.
Объясняется это тем, что оператор “=” выполняет сопоставление и переменной Х ставит в соответствие структуру, изображенную на рисунке 6.3. При этом арифметическое выражение не вычисляется. Для того чтобы заставить пролог-систему выполнить необходимые вычисления, следует применить специальный оператор is. Например,
? – X is 1+2*3.
В этом случае ответ будет Х=7.
Оператор is определен как инфиксный оператор, который в общем виде записывается так:
X is Y.
Здесь Х – или число, или не конкретизированная переменная, а Y – арифметическое выражение. Если Х – число, то вычисляется значение выражения Y, которое сравнивается с X. При равенстве значений X и Y попытка доказательства Х is Y заканчивается успехом. Если Х – не конкретизированная переменная, то вычисленное значение выражения Y приписывается переменной X. Примеры:
?– 3 is 1+2. Yes
?– X is 7-2. X=5
?– 1+2 is 1+2. No
?– X=1, X is X+1. No
Рассмотрим еще один пример. Определим функцию двух переменных f(x, y)=x+y+1:
322
Глава 6
f(X,Y,F):- F is X+Y+1.
Тогда в ответ на вопрос
? – f(1, 2, F).
будут получен ответ F=4. Однако доказательство целевого утверждения
? – f(X,2,4).
завершится неудачей, так как оператор is “работает” только в направлении справа налево.
Точно так же, как и оператор is, операторы сравнения вычисляют свои аргументы перед выполнением:
?– 5+3-1>2+1. Yes
Вбольшинстве современных реализаций языка Пролог имеются встроенные предикаты для вычисления элементарных функций: возведение в квадрат (sqr), логарифм (ln), синус (sin), косинус (cos), абсолютное значение (abs) и др.
6.5.Списки и рекурсия
Список – это структура данных, составленная их произвольного числа элементов. Элементы списка отделяются друг от друга запятыми и заключаются в квадратные скобки. Например, список из четырех элементов 'a', 'b', 'c', 'd' на языке Пролог запишется в виде:
['a', 'b', 'c', 'd'].
Список полезно рассматривать как структуру данных, состоящую из двух частей: головы списка и хвоста списка. Для приведенного выше списка элемент 'a' – это голова списка, а подсписок ['b', 'c', 'd'] – хвост списка.
Во внутреннем представлении списки – это структуры, состоящие из головы и хвоста. В качестве функтора, объединяющего голову и хвост, используется “.” . Поэтому список можно представить в виде:
.(Голова, Хвост)
Представляя список ['a', 'b', 'c', 'd'] в виде структуры, получаем
.(a, . (b, . (c, . (d, [ ] ))))
Язык Пролог
323
Здесь пара квадратных скобок [ ] обозначает пустой список.
Для представления списка в виде структуры данных, состоящей из головы и хвоста, в Прологе широко используется еще одно обозначение, в котором голова и хвост списка отделяются вертикальной чертой. Например, в записи [H|T] переменная Н – представляет голову списка, а переменная Т – хвост. Применив символ “|”, список ['a', 'b', 'c', 'd'] можно представить следующими различными способами:
Над списками часто выполняют следующие операции: добавление элемента в список, удаление элемента из списка, объединение списков, поиск элемента в списке.
Наиболее просто определяется предикат, осуществляющий добавление элемента в список:
добавить (X, L, [X|L]).
Здесь Х – добавляемый элемент; L – список, в который добавляется элемент; [X|L] – результирующий список. Таким образом, элемент Х добавляется в начало списка L.
Представление списков в виде головы и хвоста, где хвост, в свою очередь, тоже список, является рекурсивным. Поэтому обработка списков часто выполняется с помощью рекурсивных предикатов. Рекурсивными называют предикаты, в определениях которых содержатся ссылки на самих себя. Примеры простейших рекурсивных предикатов, выполняющих поиск элемента в списке и объединение двух списков, были приведены в §4.1.8. В общем случае все такие определения строятся по следующей схеме [53]:
предикат(…[ ]…). предикат(…[Голова|Хвост]…): –
обработка(Голова), предикат(…[Хвост]…).
Рекурсивные вызовы прекратятся, когда хвост списка окажется пустым списком. В приведенном определении первое утверждение определяет условие выхода из рекурсии, а второе утверждение – правило, предусматривающее при каждом вызове обработку очередного элемента списка и ре-
324
Глава 6
курсивный вызов определяемого предиката, аргументом которого является хвост списка.
Определим, например, предикат реверс(Х,Y), где список Y представляет инверсную копию списка Х:
реверс([ ], [ ]).
%1
реверс([H|T],Y): – реверс(T,Ys),
%2
append(Ys,[H],Y).
Первое утверждение фиксирует условия выхода из рекурсии, a второе – определяет правило преобразования списка Х в список Y. Для того чтобы выполнить инверсию списка [H|T] , необходимо выполнить инверсию хвоста Т и получить список Ys, а затем добавить голову Н в конец списка Ys и получить результирующий список Y. Интересно отметить, что предикат реверс(Х,Y) позволяет не только выполнять преобразование списка Х в список Y, но и осуществлять обратные преобразования. Например, на вопрос
? – реверс(Х, [1, 2, 3]).
будет получен ответ Х=[3, 2, 1].
Удаление элемента Х из списка L можно представить в виде предиката
удалить(X, L, L1),
где L1 – результирующий список. Если Х является головой списка L, то результирующий список L1 – это хвост списка L. Если Х находится в хвосте списка L, то для удаления Х необходимо рекурсивно вызвать предикат удалить, подставив в качестве второго аргумента хвост списка L. Данным правилам будет соответствовать следующее определение [7]
Предикат удалить можно использовать и в обратном порядке. Напри-
мер,
? – удалить (1, L, ['a', 'b']). L=[1, 'a', 'b'];
L=['a', 1, 'b'];
L=['a', 'b', 1].
В данном случае выполняется вставка элемента 1 в произвольные позиции списка. В итоге получаются различные списки L, исключив из которых элемент 1, получим список ['a', 'b'].
Язык Пролог
325
6.6.Управление возвратом (отсечение)
ВПролог имеется встроенный предикат, ограничивающий возвраты.
Сэтой целью отсекаются некоторые ветви дерева вывода, к которым возможен возврат. Предикат, обеспечивающий отсечение, обозначается знаком “!” . Рассмотрим его применение на простых примерах.
Определим предикат abs(X,Y), присваивающий переменной Y абсолютное значение Х:
Y
X ,
X 0
иначе.
X ,
Попробуем непосредственно реализовать данное выражение на Прологе, выделив случай Х 0:
% Определение1 abs(X,Y): – X>=0, Y is X;
Y is –X.
Проверим данное определение на целевом утверждении
? – abs(3, -3).
Пролог-система подставит вместо Х значение 3, а вместо Y – значение -3и попытается выяснить выполнимость условий 3>=0, -3 is 3. Так как второе условие не выполнимо, то пролог-система выполнит возврат и проверит альтернативную версию Y is -X, то есть -3 is -3. В этом случае выполнение предиката is завершится удачей. Следовательно, в ответ на вопрос abs(3,-3)будет получен ошибочный ответ Yes (да).
Попробуем исправить ошибку. Определим предикат abs(X,Y) следующим образом:
% определение 2 abs(X, Y): – X>=0, Y is X;
X<0, Y is –X.
Теперь при ответе на вопрос
? – abs(3, -3).
будет получен ответ No (нет). Определение 2 будет корректно и при других значениях аргументов X и Y. Однако возникает вопрос об его эффективности. В данном определении выполняются две проверки: Х>=0 и Х>0. В то же время ясно, что при выполнении условия X>=0 вторая строка опре-
326
Глава 6
деления 2 не должна выполняться. Для того чтобы пролог-системе сообщить это, воспользуемся предикатом отсечения. Введем третье определение предиката abs(X,Y):
% определение 3
abs(X, Y): - X>=0, !, Y is X; Y is -X.
Предикат отсечения “!” всегда успешно выполняется. Использование предиката “!” требует осторожности, так как после его выполнения невозможен возврат к точкам выбора, порождаемым определением, в котором используется предикат отсечения.
Рассмотрим дерево вывода целевого утверждения ? – abs(3, -3),изображенное на рисунке 6.4. Здесь показано, что определение 3 порождает альтернативы, которым соответствуют две ветви, исходящие из узла ? – abs(3, -3). После успешной проверки условия 3>=0, выполняется предикат отсечения, который стирает альтернативную ветвь определения. Теперь, после неудачной проверки выполнимости утверждения -3 is 3, про- лог-система не имеет возможности вернуться к альтернативной версии и возвращает в качестве результата значение No (нет). Таким образом, в рассмотренном примере предикат отсечения был использован для программирования взаимоисключающих версий определения.
Предикат отсечения применяется также для прерывания рекурсивных вызовов и устранения бесконечных циклов. В качестве примера рассмот-
рим программу, обеспечивающую вычисление квадратного корня y x . Для этого воспользуемся итерационной формулой:
y
n
y
n 1
1
(
x
y
n 1
),
yn 1
2
(6.1)
y0
1,
где yn – значение корня на n-ом шаге вычислений, y0=1 – начальное приближение. Вычисления заканчиваются, когда будет достигнута заданная
точность вычислений yn yn 1 . Пусть =10-5. Определим предикат
квадратный_корень(X,Y):
квадратный_корень(X,Y): –
X>0, !, поиск_квадратного_корня(X, Корень, 1), ( Y is Корень ;
Y is -Корень); Х=:=0, Y is 0.
В данном определении учитываются два возможных варианта вычисления корня. Если X>0, то с помощью предиката поиск_квадратного_корня(…) осуществляется поиск значения корня с заданной точностью. Если Х=0, то Y присваивается нулевое значение. Предикат отсечения здесь используется для программирования указанных взаимоисключающих вариантов вычислений. Покажем, как с помощью предиката отсечения прерываются рекурсивные вызовы. Определим для этого предикат
поиск_квадратного_корня(Х, Корень, Приближение).
поиск_квадратого_корня(Х, Корень, Приближение): – Yn is (X/ Приближение+Приближение)/2,
(abs(Yn - Приближение)< 1.0e-5, !, Корень is Yn;
поиск_квадратного_корня(Х, Корень, Yn)).
Здесь переменная Приближение соответствует yn-1в формуле (6.1). Когда абсолютное значение разности между yn-yn-1станет меньше 10-5, то с помощью предиката отсечения стирается альтернативная ветвь, соответствующая рекурсивному вызову предиката поиск_квадратного_корня. Переменной Корень присваивается текущее значение Yn, и происходит выход из рекурсии.
В определениях рассмотренных выше предикатов использована группа. Группа – это заключенная в скобки последовательность вариантов, от-
328
Глава 6
деленных друг от друга точкой с запятой. Группа позволяет, с одной стороны, рассматривать совокупность вариантов как одно утверждение, а с другой – ограничивать область действия связки ”или”. По существу, группа – это тело правила без головы.
Уточним область действия предиката отсечения. Рассмотрим пример. Определим предикат абсолютный_элемент(X,L), который будет иметь истинное значение, если список L будет содержать числовой элемент, абсолютное значение которого равно Х. Приведем два определения этого предиката:
%Определение 1 абсолютный_элемент(X, L): –
элемент(E, L), abs(E, X).
%Определение 2
абсолютный_элемент(X, L): –
элемент(E, L), (E>=0, !, X is E;
X is –E).
Во втором определении предикат abs(E,X) заменен группой, которая представляет его тело. Предикат элемент(E,L) соответствует предикату member(E,L), определенному в §4.1.7. Проверим данные определения на вопросе:
? – абсолютный_элемент(3, [2, -3]).
При использовании первого определения ответ на вопрос будет положительным, а применение второго определения даст отрицательный ответ No (нет). Происходит это из-за того, что предикат отсечения расширил область своего действия. В первом определении он действует локально в пределах определения abs(E,X). Во втором определении областью его действия является весь предикат абсолютный_элемент(X,L). Поясним сказанное с помощью дерева вывода, изображенного на рисунке 6.5.
Как видно из рисунка, предикат “!” отсекает не только альтернативные ветви, порождаемые определением abs(E,X), но и ветви, обусловленные недетерминированным исполнением предиката элемент. Поэтому правильным будет первое определение предиката абсолютный_элемент.
В заключение отметим, что введение предиката отсечения – это отступление от логических принципов. Отсечение делает программы чувствительными к порядку следования предложений, что нарушает декларативное восприятие программы. Поэтому при программировании на Прологе следует избегать применения отсечения без достаточных оснований.
Здесь предикат не_элемент определяется через предикат элемент. Если подцель элемент(X,L) доказуема, то отсечение исключает выбор альтернативы, а fail вызывает неудачу. Иначе считается, что цель не_элемент(X, L) достижима (true – успех). Такое определение предиката не_элемент основано на использовании предположения о замкнутости мира. При этом под миром понимается множество утверждений конкретной программы. Полагается, что в этих утверждениях содержатся все знания о решаемой задаче. Если некоторое утверждение не представлено в программе, то считается, что оно ложно. В этом случае доказательство истинности исходного утверждения подменяется доказательством недоказуемости противоположного утверждения, что не одно и то же. Поэтому рассмотренной схемой