1. Введение

advertisement
1. Введение
Лучшим каждому кажется то,
к чему он имеет охоту.
Козьма Прутков. Мысли и афоризмы
Цель написания любой программы для ЭВМ заключается в построении такой модели предметной области, которую можно использовать для
решения некоторых возникающих в этой области задач. Качество модели
при этом определяется как способностями программиста, так и тем набором инструментов (языков программирования), которые он использует.
Очевидно, что при прочих равных условиях выбор языка может иметь
решающее значение. Смысл программы на традиционном процедурном
языке определяется в терминах поведения компьютера при исполнении
этой программы, в то время как исходное понимание задачи не имеет машинно-ориентированной специфики. Поэтому неформальный этап разработки и реализации алгоритма решения требует от программиста значительных затрат времени и высокой квалификации.
Суть идеи логического программирования состоит в том, что компьютеру в качестве программы можно предоставить не алгоритм, а формальное описание предметной области в виде системы отношений между
ее объектами (реляционную модель), поручив решение задачи на этой
модели самому компьютеру. Тогда при наличии эффективного метода
автоматизации доказательства (машины вывода) задача программиста
сводится к аксиоматизации предметной области в машинно-независимых
терминах. Практическая эффективность логического программирования
при этом по-прежнему зависит как от удачной аксиоматизации задачи, так
и (в еще большей степени) от качества интерпретатора, автоматизирующего доказательство. Однако машинно-независимый уровень описания
модели задачи обещает существенную экономию усилий программиста.
Наиболее известная и эффективная реализация идей логического программирования воплощена в языке Пролог 1, который давно и успешно
применяется для решения целого ряда задач. Среди них
 разработка быстрых прототипов приложений,
 автоматизация перевода естественных и искусственных языков,
 разработка естественно-языковых интерфейсов,
 создание экспертных систем и оболочек,
 символьное интегрирование, дифференцирование и решение уравнений,
 доказательство теорем.
Логическое программирование не началось с языка Пролог и не заканчивается на нем. Существуют и иные языки, в частности, дающие возможность реализовать возможности параллельной архитектуры ЭВМ.
3
1
В данном учебном пособии рассмотрены математические основы логического программирования, показаны возможности и ограничения реализации логических программ в среде ТурбоПролог, рассмотрены примеры использования этого языка для разработки быстрого прототипа системы качественного моделирования, а также реализация версии Прологмашины на языке C++.
2. Основы логического программирования
Язык логики предикатов
После ряда наблюдений я установил с
исключительной точностью, что
каждая селедка – рыба, но не каждая
рыба – селедка.
А. Некрасов. Приключения капитана Врунгеля
Формальное определение языка логики предикатов включает в себя
следующие правила.
 Терм – константа, переменная или кортеж f(x1,…xn) из n>0 термов xi,
перед которым стоит функциональный символ (функтор) f.
 Предикат P(x1,…,xn) – кортеж из n0 термов xi, перед которым стоит
предикатный символ P. Предикат является атомарным элементом
языка, имеющим истинностное значение.
 Формула – предикат или выражение, построенное из формул при помощи логических функций (табл.1) 1 и кванторов (табл. 2).
 Предложение - это формула, в которой каждая входящая переменная
находится в области действия квантора по этой переменной.
Множество всех предложений, построенных согласно вышеприведенным правилам, образует язык логики предикатов первого порядка. В
этом языке термы используются для обозначения конкретных (константы)
или абстрактных (переменные) объектов предметной области. Например,
терм
книга(«Толкиен», «Властелин колец»)
описывает конкретный структурированный объект книга, атрибутами которого являются автор и название, заданные в виде текстовых констант, терм
Формально можно определить 22=4 одноместных логических функций и
24=16 логических функций двух аргументов (показатель степени соответствует количеству комбинаций возможных значений аргументов). Однако
на практике используется лишь базовое подмножество функций, представленное в табл. 2.1. Остальные функции специальных названий не
имеют, но при необходимости легко могут быть выражены в этой базе.
4
1
книга(«Толкиен»,Y)
описывает абстрактный объект (некая книга Толкиена). Предикаты определяют отношения между объектами предметной области:
читает(человек, книга),
а предложения описывают логические свойства этих отношений:
читает(X, книга(«Гофман»,Y))  любит(X, «сказки»)
и в совокупности образуют аксиоматическую теорию.
Метод резолюций
На удочку насаживайте ложь
И подцепляйте правду на приманку.
В. Шекспир. Гамлет
Язык логики предикатов, помимо бесспорных преимуществ над алгоритмическими языками в части формализации описания предметной
области1, представляет собой исключительно благоприятную среду для
автоматизации вывода. Наиболее подходящим для автоматизации вывода
является так называемый метод резолюции. Суть его состоит в добавлении к аксиоматической теории T отрицания доказываемого утверждения
Q (запроса) и последующих систематических попытках доказательства
непоследовательности теории TQ, которая формально устанавливается
по наличию фразы вида Q & Q. Наличие этой фразы означает, что дополнение теории отрицанием доказываемого утверждения привело к противоречию, и, следовательно, Q является следствием T. Напротив, неудача
резолюции означает, что утверждение Q недоказуемо (но не ложно –
своеобразная «презумпция невиновности»!) в теории T. Этот подход является общепринятым в математике и носит название доказательства от
противного (reductio ad absurdum): отрицание опровергается путем демонстрации того, что его допущение ведет к противоречию. Метод резолюции применяется к подмножеству предложений, называемых фразами
Хорна. Фразы Хорна – это предложения, построенные из положительных
и отрицательных литералов2 при помощи связок “&” и “”, причем положительный литерал, если он присутствует в предложении, должен быть
единственным. Фразы Хорна вида:
A B C D
A
 B C D
Имеется в виду отсутствие каких-либо предписаний по обработке модели, а также зарезервированных слов, специфичных для конкретного алгоритмического языка.
2
Положительным и отрицательным литералами называются соответственно предикаты и их отрицания.
5
1
называются соответственно правилом, фактом и запросом, и в результате
тождественных преобразований (см. табл. 1) могут быть переписаны в
удобном для прочтения виде1:
A:- B, C, D
A
 (B, C, D)
Аксиоматическая теория, в контексте которой выполняется доказательство запроса по методу резолюции, содержит только предложения в
формате фактов и правил. В качестве примера представления теории
множеством фраз Хорна рассмотрим следующие определения:
Мужчина(«Василий»).
Мужчина(«Сергей»).
Мужчина(«Александр»).
Мужчина(«Лев»).
Женщина(«Надежда»).
Женщина(«Ольга»).
Отец(«Сергей», «Александр»).
Отец(«Сергей», «Лев»).
Отец(«Сергей», «Ольга»).
Мать(«Надежда», «Александр»).
Мать(«Надежда», «Лев»).
Мать(«Надежда», «Ольга»).
Брат(X,Y):- Мужчина(X), Отец(F,X), Мать(M,X), Мужчина(Y), X<>Y,
Отец(F,Y), Мать(M,Y).
Приведенные определения включают в себя факты Мужчина, Женщина, Отец, Мать, описывающие отношения между конкретными людьми (в данном случае – родственниками Пушкина), и общезначимое правило2, определенное в терминах абстрактных объектов – переменных. Семантика фактов очевидна, правило же утверждает, что братьями являются
два любых (внешний квантор общности, связывающий переменные правила, не показан) различных мужчины, имеющих общего отца и общую
мать.
Связку “” в записи логических программ обычно заменяют символом
“:-”, связку “&” – символом “,” а связку “” - символом “;”.
2
Возможны и общезначимые факты: например, предложение Равен(X,X)
можно интерпретировать как утверждение о том, что любой объект равен
сам себе.
6
1
Таблица 2.1
Аргументы
X1
X2
0
1
0
0
1
1
0
1
0
1
Отрицание
Конъюнкция

1
0
-
&
0
0
0
1
Логические функции
Функции
ДизъНеравноРавноюнкция
значзначность
ность



0
0
1
1
1
0
1
1
0
1
0
1
Импликация
Стрелка
Пирса
Штрих
Шеффера

1
1
0
1

1
0
0
0

1
1
1
0
Таблица 2.2
Наименование квантора
Общности
Существования
Обозначение


Кванторы
Пример использования
(X)1 селедка(X)рыба(X)
(X) рыба(X)селедка (X)
Интерпретация
Всякая селедка – рыба,
но не всякая рыба – селедка
Для краткости внешний квантор общности обычно опускают, поэтому предложение
селедка (X) рыба (X) эквивалентно приведенному
1
7
Множество фактов, устанавливающих отношения между константными объектами, иногда называют базой данных, а все прочие предложения - базой знаний логической программы. Именно наличие в логической
программе общезначимых предложений обеспечивает возможность вывода (путем доказательства) информации, отсутствующей в программе в
явном виде. Так, результатом применения метода резолюции к представленной теории, дополненной запросом  Брат(X, Y), будут два решения:
X = «Александр», Y = «Лев»,
X = «Лев», Y = «Александр».
Содержательный смысл полученных решений очевиден, однако имеется также ряд не столь очевидных, но исключительно важных для понимания природы логического программирования обстоятельств.
 Метод резолюции обнаружил все возможные формально различные
следствия заданной посылки. Это свойство является одним из аспектов недетерминированности (отсутствия факторов, точно определяющих ход исполнения) логической программы.
 Последовательность решений на самом деле несущественна, так как
все утверждения теории и все следствия из нее истинны одновременно вне зависимости от порядка их включения в теорию. Приведенный
порядок характерен для последовательной (фон-неймановской) реализации машины вывода.
 Метод резолюции не смог доказать “братских” отношений между
Сергеем и Василием, так как соответствующая информация теории
недоступна. Это еще раз подчеркивает разницу между “недоказуемо”
и “ложно”.
 Результат доказательства является логическим следствием теории.
Вся необходимая для получения результата информация содержалась
в теории в неявном виде изначально, механизм доказательства только
“решил уравнения относительно неизвестных”. Отсутствие побочных
эффектов изменения среды вычислений – характерная особенность
логических программ, реализующих дедуктивный метод вывода частных следствий из общих посылок.
Рассмотрим теперь алгоритм резолюции на аксиоматической теории,
составленной из фраз Хорна, более подробно, для чего введем необходимые понятия.
Переменные логической программы, которые в ходе выполнения алгоритма резолюции прямо или косвенно адресуют константные термы,
называются связанными. Переменные, значения которых механизму вывода в данный момент неизвестны, называются свободными. Отметим, что
статус переменой в процессе доказательства может изменяться.
Под унификацией в логическом программировании понимается процедура попарного сопоставления термов, подчиняющаяся следующим
правилам.
8
Константа может быть унифицирована сама с собой или со свободной
переменной.
 Свободная переменная может быть унифицирована с любым термом.
 Функциональный терм унифицируется с другим функциональным
термом при условии, что оба имеют один и тот же функтор и одинаковое количество попарно унифицируемых аргументов.
В результате унификации константы или связанной переменной со свободной переменной последняя приобретает статус связанной. Таким образом, механизм унификации обеспечивает выполнение трех функций:
 Логической проверки равенства.
 “Присваивания”1 значений переменным.
 Анализа структур данных.
Элементарным шагом доказательства в методе резолюций является
построение резольвенты (от resolve – разрешать). Резольвента – это фраза
Хорна, построенная из двух исходных фраз, одна из которых имеет положительный P, а другая – одноименный отрицательный  P литерал, причем аргументы этих двух литералов попарно унифицируемы, путем объединения всех литералов исходных фраз за исключением P и  P.
Например, резольвентой фраз  P и P R S (при условии, что аргументы P и  P попарно унифицируемы) будет фраза  RS, резольвентой P и P будет, очевидно, пустая фраза (противоречие).
Алгоритм резолюции строит первую резольвенту с участием запроса, для которого последовательным просмотром предложений теории
подыскивается подходящее для резольвирования, а все последующие –
аналогичным способом с использованием резольвенты, полученной на
предыдущем шаге. Процесс построения резольвент и сопутствующего
связывания переменных продолжается до тех пор, пока не будет построена пустая фраза, либо построение очередной резольвенты окажется невозможным2. В первом случае принято говорить об успехе вычислений,
которое обозначается меткой □, а во втором – об их неудаче, обозначаемой меткой ■. При успехе вычислений множество связанных переменных
запроса представляет собой вариант решения, который может быть
направлен на устройство вывода (единственный существенно необходимый побочный эффект!). При неудаче никаких сообщений не формируется. И в том, и в другом случае алгоритм резолюции продолжает работу,

Речь идет не о разрушающем присваивании, свойственном процедурным
языкам, так как логическая программа не изменяет среду исполнения.
Связанная переменная лишь указывает на объект, существующий с момента написания логической программы.
2
Предполагается, что программа не приводит к бесконечным вычислениям, как это имеет место, например, в следующем случае: Больше(A,B):Меньше(B,A). Меньше(A,B):-Больше(B,A).
9
1
пытаясь найти другие варианты построения резольвент, начиная с последней. При этом все связывания переменных, имевшие место в результате прежнего резольвирования, отменяются, и соответствующие переменные вновь становятся свободными. Этот процесс называется возвратом. Процесс построения резольвент и возврата продолжается до тех пор,
пока не останется неисследованных возможностей построения резольвент.
Таким образом, порождается дерево вычислений, левосторонний фрагмент которого для доказательства запроса Брат(X, Y) представлен на рис.
2.1.
? Брат (X,Y)
? Мужчина (X)
X=’Василий’
X=’Сергей’
? Отец (F,X)
X=’Александр’
? Отец (F,X)
? Отец (F,X)
F=’Сергей’
? Мать (M,X)
M=’Надежда’
? Мужчина (Y)
Y=’Василий’
Y=’Сергей’
Y=’Александр’
? X<>Y
? X<>Y
? Отец (F,Y)
? Отец (F,Y)
? X<>Y
Y=’Лев’
? X<>Y
? Отец (F,Y)
? Мать (M,Y)
Рис. 2.1. Дерево вычислений
10
3. Реализация идей логического программирования
в языке Пролог
Он не был свет, но был послан,
Чтобы свидетельствовать о Свете.
Евангелие от Иоанна, глава 1, стих 8.
Вслед за разработанной в начале 70-х годов в университете Марселя
(Франция) Аланом Колмероэ первой официальной версией языка, воплощающего идеи ПРОграммирования ЛОГики и названного по этой причине Пролог, последовал целый ряд успешных реализаций Пролога для
различных аппаратных платформ и операционных сред. Возможности
этого языка в этом и следующем разделах иллюстрирует версия Турбо
Пролог 2.0, реализованная фирмой Borland International на платформе PC
– совместимых компьютеров. Следует иметь в виду, что основная цель,
которую преследовали авторы, состоит в демонстрации возможностей и
ограничений Пролога как языка логического программирования. Исчерпывающее описание языка и среды Турбо Пролог можно найти в [1].
Структура программы на языке Турбо Пролог
Программа на языке Турбо Пролог состоит из пяти необязательных
(еще одно косвенное подтверждение высокого уровня языка!) секций1.
 Секция декларации термов (domains) содержит определения типов
аргументов, используемых предикатами программы. По смысловому
значению содержимое этой секции соответствует декларации типов
данных в языке Паскаль. Так же, как и в Паскале, определения предикатов могут ссылаться на предопределенные типы символов (char),
целых (integer) и вещественных (real) чисел и строк (string, symbol).
Если программист не определяет собственных типов, данная секция
может отсутствовать.
 Секция декларации предикатов (predicates) определяет предикаты как
отношения между объектами, каждый из которых принадлежит к одному из предопределенных или декларированных пользователем типов. Аналогом декларации предикатов может служить декларация
прототипов процедур в алгоритмических языках. Турбо Пролог допускает множественные декларации одного предиката при условии,
что они следуют подряд и имеют одинаковое количество аргументов.
Среда Турбо Пролог также предоставляет пользователю набор встроенных предикатов, служащих для повышения эффективности доказательства (типичный пример – встроенная арифметика), управления
Каждая из секций, кроме секции запроса, может быть объявлена неоднократно, при условии, что декларация понятия предшествует его использованию.
11
1
доказательством и реализации побочных эффектов (предикаты вводавывода). Если программист не определяет собственных отношений,
эта секция также может отсутствовать.
 Секция декларации предикатов базы данных (database) определяет
предикаты, утверждения для которых могут динамически добавляться
к программе, реализуя тем самым побочный эффект «самообучения».
Так как самообучение выходит за рамки чистой дедукции, реализуемой в процессе выполнения логических программ, эта возможность
далее рассматриваться не будет. Как и прежде, если программист не
планирует использование предикатов данной секции, последняя может отсутствовать.
 Секция утверждений (clauses) содержит набор предложений для декларированных предикатов в формате фактов и правил (утверждения
для одного предиката группируются). При отсутствии секции декларации предикатов данная секция также отсутствует.
 Секция запроса (goal) содержит утверждение формата запроса и также может отсутствовать. В этом случае говорят о программе с внешней целью, которая может исполняться только в среде Турбо Пролог.
Программа, содержащая секцию цели, может быть скомпилирована в
файл exe – формата. Однако поведение механизма вывода в этом случае будет отличаться от описанного выше: он обнаружит только первое решение. Чтобы заставить его искать все остальные, придется использовать специальные предикаты управления доказательством.
Таким образом, работоспособная (правда, только в среде разработки)
программа может не содержать ни единого символа и при этом выполнять
функции, например, калькулятора. Чтобы исполнить такую программу,
достаточно выбрать пункт меню Run и в окне диалога набрать, например,
X = 3, Y = sqrt(X+1). Результат исполнения предсказуем: X = 3, Y =2.
Простейшие программы
Пример 1. Как выглядит Пролог-программа?
Рассмотрим вновь определения родственных отношений:
Domains
Person = string
Predicates
Male(person)
Female(person)
Father(person)
Mother(person)
Brother(person, person)
clauses
male
male
male
male
12
(«Василий»).
(«Сергей»).
(«Александр»).
(«Лев»).
female («Надежда»).
female («Ольга»).
father («Сергей», «Александр»).
father («Сергей», «Лев»).
father («Сергей», «Ольга»).
mother («Надежда», «Александр»).
mother(«Надежда», «Лев»).
mother(«Надежда», «Ольга»).
brother(X,Y):male(X), father(F,X), mother(M,X),
male(Y), X<>Y, father(F,Y), mother(M,Y).
Отметим следующие особенности записи программы:
для именования термов, предикатов и переменных можно использовать только символы из первой половины кодовой таблицы ASCII;
 псевдоним стандартного домена string – person введен для улучшения
читаемости программы;
 все предикаты пользователя необходимо декларировать;
 строковые константы заключаются в кавычки;
 имена переменных должны начинаться с прописной буквы;
 областью видимости переменной является текущее утверждение Пролога;
 встроенный предикат <> использует традиционную инфиксную форму записи;
 секция запроса отсутствует, поэтому программа может исполняться
только в среде Турбо Пролог.
Если выбрать пункт меню Run и в окне диалога среды Турбо Пролог
ввести запрос Brother(X, Y), результат его согласования будет аналогичным приведенному в разделе 1. Возможны также и другие запросы - о
братьях Александра - Brother («Александр»,X), о мужчинах семьи Пушкиных male(X) и т.п.
Отметим, что, в отличие от заголовков процедур в алгоритмических
языках, закрепляющих за каждым аргументом конкретный входовыходной статус, заголовки утверждений Пролога могут унифицироваться с различными вариантами разметки аргументов запроса (далее для
краткости – i/o разметки, где i соответствует связанной переменной или
константе, а o – свободной переменной). Так, утверждение предиката
Brother успешно согласуется при любой разметке (o,o), (i,o), (o,i), (i,i).
Отметим также возможность использования в утверждениях Пролога так называемых анонимных переменных, служащих для обозначения
объектов, которые интересуют нас только фактом своего существования,
но не возможным значением. Для обозначения анонимных переменных
используется символ подчеркивания. Например, запрос «Есть ли братья у
Александра?» формулируется с использованием анонимной переменной
так: Brother («Александр»,_ ). Ответом среды будет Yes. Хотя все анонимные переменные обозначаются одинаково, они адресуют различные объ13

екты. Например, в запросе Brother (_ ,_ ) – “есть ли братья в семье Пушкиных” – две анонимные переменные различны.
Если приведенную программу дополнить секцией запроса (“внутренней” целью), машина вывода обнаружит только первое решение (хотя
встроенные предикаты управления доказательством позволяют решить
эту проблему), увидеть которое можно с помощью реализующего побочный эффект вывода встроенного предиката write:
Goal
Brother(X,Y), write(X,Y), nl.
Представленный здесь запрос состоит из нескольких подцелей и
называется сложным.
Пример 2. Что такое декларативный стиль программирования?
Рассмотрим пример определения структуры вентильной схемы (рис.
3.1). Ее реляционная модель представляет собой систему отношений, которые устанавливают логические элементы (предикаты) между пронумерованными узлами схемы (термами):
&
1
2
5
7
3
4
&
Рис. 3.1 Вентильная схема.
Domains
Node = integer
Predicates
Not_(node, node)
And_(node, node, node)
Or_(node, node, node)
Xor_(node, node, node)
Clauses
Not_(1, 2).
Not_(3, 4).
And_(2, 3, 5).
And_(1, 4, 6).
Or_(5, 6, 7).
Xor_(A, B, C):Not_(A, D),
Not_(B, E),
And_(D, B, F),
And_(A, E, G),
Or_(F, G, C).
14
6
Чтобы исключить конфликт со встроенными логическими предикатами Турбо Пролога, имена структурных предикатов в программе дополнены символом подчеркивания. Здесь определены три базовых предиката
– «НЕ», «И», «ИЛИ»: Not_, And_, Or_, в терминах которых описана структура схемы, и один производный – «разделительное ИЛИ»: Xor_, абстрактная внутренняя структура которого также определена в терминах
базовых предикатов. Приведенный пример – это идеальная логическая
программа, которая только определяет (декларирует) отношения между
объектами. Возможные следствия этих деклараций в контексте конкретного запроса выводятся Пролог-машиной автоматически. Например, запрос Xor_(_, _, _) позволит установить, имеются ли фрагменты схемы,
структура которых соответствует определению предиката Xor_, а запрос
Xor_(X, Y, Z) даст ответ на вопрос о “привязке” таких элементов к конкретным узлам схемы. Результат согласования последнего запроса с определениями программы: X = 1, Y = 2, Z = 7. Решение нетривиальной задачи
анализа структуры не требует от пользователя никаких усилий (правда,
время, которое Пролог-машина затратит на вывод, будет расти экспоненциально с ростом сложности схемы). Такой стиль написания программ
называется декларативным.
Рекурсия
У попа была собака, он ее любил.
Она съела кусок мяса, он ее убил.
В землю закопал и надпись написал:
У попа была собака …
Фольклор
Как известно, рекурсия – это определение, которое содержит определяемое понятие. Сила рекурсии в том, что она позволяет конечным числом определений определить бесконечное число фактов. В Прологе, языке
деклараций, рекурсия играет настолько важную роль, что это дает основания называть его существенно рекурсивным языком. Источниками рекурсии в Прологе являются рекурсивные утверждения и рекурсивные структуры данных.
Рассмотрим примеры рекурсии, обусловленной характером определений.
Пример 3. Определение факториала
Классическое определение: факториал нуля равен единице; факториал N>0 равен произведению N на факториал N-1. Его запись на языке
Пролог изоморфна оригиналу:
Predicates
Factorial(integer)
15
Clauses
Factorial(0, 1).
Factorial(N,F):N1 = N-1,
Factorial(N1, F1),
F = N*F1.
Обратим внимание читателя на то, что программа
не содержит секции domains, так как не определяет пользовательских
типов;
 использует предикаты «-», «*», «=» встроенной арифметики Пролога;
 использует предикат N1 = N-1, а не тождественно ложный N = N-1,
так как понятие присваивания, сбивающее с толку начинающих программистов, в Прологе отсутствует.
При написании рекурсивных определений необходимо следить, чтобы они были конструктивными, иначе возникнет ситуация переполнения
стека. Для этого, во-первых, среди утверждений должно присутствовать
по крайней мере одно, которое не порождает рекурсии – своеобразное
«граничное условие». В нашем примере это факт Factorial(0,1). Вовторых, все остальные утверждения должны систематически сводить исходную задачу к аналогичной подзадаче (мы определяем факториал числа
N через факториал числа, на единицу меньшего), обеспечивая в конечном
счете выполнение одного из граничных условий.
Предикат Factorial поддерживает два варианта i/o– разметки: (i, i), (i,
o), соответствующие запросам о равенстве факториала заданного числа
заданному значению и о значении факториала заданного числа. Оставшиеся два варианта – (o, i), (o, o), соответствующие запросам о значении
числа, факториал которого задан, и о значении факториалов всех чисел, не поддерживаются. Это своеобразная «плата» за повышение эффективности, вызванное использованием в определениях предикатов встроенной
арифметики. Создание универсального определения факториала с помощью предикатов пользовательской арифметики теоретически возможно,
но непрактично, так как приводит к необходимости определения непомерного количества фактов.

Пример 4. Ханойские башни
Суть этой известной головоломки (рис. 3.2) заключается в определении порядка перемещения N дисков разного диаметра с одного (исходного) стержня на другой (целевой) при наличии третьего (резервного) и при
выполнении следующих ограничений.
 Диски перемещаются по одному;
 В процессе перемещения диск большего диаметра не должен оказаться поверх диска меньшего диаметра (расположение дисков на исходном стержне удовлетворяет ограничению).
16
Рис. 3.2. Ханойские башни
Дадим рекурсивное определение порядка перемещения дисков. Для
этого, следуя сформулированным выше рекомендациям, определим «граничные условия» и схему сведения задачи к подзадачам. Очевидно, что
«граничное условие» соответствует элементарной задаче перемещения
единственного диска. Тогда задачу перемещения N дисков можно разбить
на подзадачи перемещения N-1 диска с исходного стержня на запасной,
последнего диска на целевой стержень и N-1 диска с запасного стержня на
целевой:
domains
node = Левый;Средний;Правый
predicates
hanoi(integer, node, node, node)
clauses
hanoi(1,A,_,C):-nl,write(A,"->",C).
hanoi(N,A,B,C):- N>1,N1=N-1, hanoi(N1,A,C,B),
hanoi(1,A,B,C), hanoi(N1,B,A,C).
Здесь нуждается в комментарии определение терма node в виде
списка констант, разделенных альтернативой или (символ «;»). Для демонстрации последовательности действий в предложенной версии использован (пока) побочный эффект вывода (предикаты nl и write). К определениям этой задачи, не содержащим побочных эффектов, мы вернемся
после знакомства с рекурсивными структурами данных. Ниже приведен
протокол вывода для N = 3:
Левый -> Правый
Левый -> Средний
Правый -> Средний
Левый -> Правый
Средний -> Левый
Средний -> Правый
Левый -> Правый
Рассмотрим теперь рекурсивные определения данных.
Пример 5. Дать определение линейного списка и предиката проверки
принадлежности элемента данному списку
domains
list = list(integer,list);empty
predicates
17
member(integer, list)
clauses
member(X,list(X,_)).
member(X,list(_,L)):-member(X,L).
Приведенная программа содержит ряд нововведений. Во-первых, в
ней определен структурный терм list – список (это не зарезервированное
слово). Определение терма содержит альтернативу или. Таким образом,
терм определяется либо как структура, состоящая из целого и списка, либо как константа empty – пустой (также не зарезервированное слово). Втретьих, рекурсивное определение принадлежности обусловлено исключительно структурой терма. Его смысл в том, что элемент принадлежит
списку, если он совпадает с текущим элементом этого списка (первое
утверждение member) либо принадлежит оставшейся части списка (второе
утверждение member). Использование анонимных переменных является
следствием того, что в первом случае нас не интересует оставшаяся часть
списка, а во втором – его текущий элемент. Интересно отметить, что
определенная нами семантика предиката – проверка принадлежности – не
отражает в полной мере возможностей, которые он демонстрирует. На
самом деле он поддерживает два варианта i/o разметки: (i, i) и (o, i). Это
означает, что помимо проверки принадлежности данного элемента данному списку (i, i) можно проверить, какие вообще элементы принадлежат
данному списку (o, i). В сказанном можно убедиться, выполнив следующие запросы:
member(5, list(3,list(5,list(7,empty))))
и
member(Х, list(3,list(5,list(7,empty)))).
Последний запрос даст три решения: X = 3; X = 5; X = 7, в то время
как на первый программа только ответит Yes (Да).
Определение еще одной рекурсивной структуры – дерева – и предиката ее левостороннего обхода приведем без комментариев:
domains
tree = tree(integer,tree,tree);empty
predicates
traverse(tree)
clauses
traverse(tree(X,empty,empty)):-nl,write(X).
traverse(tree(X,T1,T2)):traverse(T1), nl,write(X), traverse(T2).
Актуальность структуры «список» (вспомним, что в языке LISP она
вообще является фундаментальной!) побудила разработчиков Пролога на
создание встроенной реализации. Функциональный аналог определений
списка и предиката принадлежности, использующий встроенную реализацию, приведен ниже.
18
domains
list = integer*
predicates
member(integer, list)
clauses
member(X,[X|_]).
member(X,[_|L]):-member(X,L).
Пример запроса:
member(X,[3,5,7]).
Используя этот пример, поясним связанную со списками терминологию. Начнем с точного определения. Список - это либо пустая (не содержащий элементов) последовательность, либо структура из головы (первый
элемент списка) и хвоста (остаток списка), где хвост также является
списком. В декларации списка указывается тип его базового элемента (в
примере - integer) и функтор списка (символ «*»). Если список рассматривается в утверждении как структурированный терм, его описание заключается в квадратные скобки. Например, [1, X, 3] – список из трех элементов, [ ]- пустой список. Над непустыми списками определена операция
расщепления на голову и хвост. Она обозначается вертикальной чертой.
Например, [1|[X, 3]] – это список [1, X, 3], расщепленный на голову и
хвост. К пустому списку расщепление неприменимо. Зато он унифицируется с любым типом списка.
Еще один популярный предикат – предикат слияния списков:
domains
List = string*
Predicates
Append(list, list, list)
Clauses
Append([],L,L).
Append([H|L1],L2,[H|L3]):-append(L1,L2,L3).
Смысл утверждений предиката append состоит в том, что результат
слияния пустого списка с любым списком L есть список L (первое утверждение), а результат слияния списков [H|L1] и L2 есть список [H|L3], если результатом слияния L1 и L2 является L3 (второе утверждение). Однако действительные возможности этого предиката значительно шире простого слияния (табл. 3.1).
Таблица 3.1
Согласование запросов с утверждениями предиката слияния
I/o разметка
(i, i, i)
(i, i, o)
(i, o, i)
(o, i, i)
(o, o, i)
Запрос
Append([“один”], [“два”],[“два”,”три”])
Append([“один”], [“два”,”три”],X)
Append([“один”],X, [“один”,“два”,”три”])
Append(X, [”три”], [“один”,“два”,”три”])
Append(X,Y,[“один”,“два”])
Результат
Согласования
No
X=[“один”, “два”,”три”]
X=[“два”, ”три”]
X=[“один”, “два”]
X=[], Y=[“один”, “два”]
X=[“один”], Y=[“два”]
19
X=[“один”, “два”], Y=[]
Первый раунд знакомства со списками завершим ревизией определений головоломки “Ханойские башни”:
domains
node = Левый;Средний;Правый
move=move(node,node)
list = move*
predicates
hanoi(integer, node, node, node,list)
append(list,list,list)
clauses
hanoi(1,A,_,C,[move(A,C)]).
hanoi(N,A,B,C,T):- N>1,N1=N-1, hanoi(N1,A,C,B,L1),
hanoi(N1,B,A,C,L2), append(L1,[move(A,C)|L2],T).
append([],L,L).
append([H|L1],L2,[H|L3]):-append(L1,L2,L3).
Новая версия лишена побочных эффектов. Результат согласования
запроса hanoi(3, Левый, Средний, Правый, L) возвращает список L последовательных операций перестановки.
Управление доказательством
Бесплатный сыр бывает только в
мышеловке.
Фольклор
Стратегия доказательства, реализуемая Пролог-машиной – это полный перебор вариантов методом поиска в глубину с возвратом. Ее несомненным достоинством является относительная простота алгоритмизации, в чем нам предстоит убедиться в главе 4. Однако за эту простоту
приходится расплачиваться комбинаторным ростом сложности доказательства с увеличением размерности задачи. Надежды на радикальное
решение этой проблемы, по-видимому, следует отложить до появления
реализаций Пролога на компьютерах с параллельной архитектурой. Частичное же решение для традиционных компьютеров заключается в
управлении процессом доказательства. Управление предполагает знание
двух базовых принципов, определяющих последовательность доказательства:
 подцели в запросе и в теле правила испытываются в порядке записи слева направо;
 утверждения для согласования с подцелями испытываются в порядке записи сверху вниз.
Разумеется, программа, написанная в расчете на определенную последовательность доказательства, не имеет права называться логической,
так как истинность теории не должна зависеть от порядка записи ее аксиом. Но стремление программиста сделать свою программу практичной
сильнее стремления к недостижимому идеалу.
20
Наиболее естественно желание программиста заблокировать поиск
последующих вариантов после того, как найдено первое решение. Такую
возможность предоставляет имеющийся практически во всех реализациях
Пролога встроенный предикат отсечение (cut), обозначаемый символом
«!» в записи утверждений. Точный смысл этого предиката заключается в
том, что он детерминирует все предшествующие ему подцели в правой
части правила, а также вызов самого правила. Имеется в виду, что для
каждой подцели потенциально существует несколько вариантов построения резольвенты. Предикат отсечения фиксирует выбор вариантов, которые позволили процессу доказательства добраться до отсечения; все последующие варианты доказательства отбрасываются. Таким образом, отсечение, подобно методу ветвей и границ, ограничивает дерево перебора,
способствуя поиску «хороших» частных случаев в «плохих» задачах. Три
типичных случая использования отсечения –
 фиксация выбора правила в процессе доказательства;
 фиксация выбора способа доказательства правой части правила;
 построение определений методом «от противного».
Рассмотрим первый случай на примере утверждений для предиката
«проверка принадлежности»:
Member(X,[X|_]):-!.
Member(X,[_|T]):-member(X,T).
По определению, наличие отсечения в правой части первого утверждения детерминирует его вызов. Других последствий не будет, так как
отсечение стоит первым и единственным в списке подцелей. Новая версия
предиката в точности соответствует своему названию, так как может служить только для проверки принадлежности элемента списку. Запрос
member(2,[1,2,3]) возвращает Yes, запрос member(X, [1,2,3]) возвращает
единственное решение X=1.
Второй случай рассмотрим на примере определения общего элемента двух списков. Оно легко строится на базе предиката проверки принадлежности:
Common_element(X,L1,L2):-member(X,L1),member(X,L2),!.
Смысл отсечения здесь в том, что если нам удалось доказать принадлежность X списку L1 и списку L2, наличие общего элемента уже доказано, и дальнейшие попытки приведут лишь к непроизводительным
затратам ресурсов. Отсечение – очень сильный инструмент, поэтому применять его следует осторожно. В частности, ответ на вопрос, какую из
двух версий предиката проверки принадлежности следует использовать в
определении общего элемента двух списков, неочевиден. Если мы хотим
удостовериться в том, что некоторый заданный элемент принадлежит
двум заданным спискам (разметка (i, i, i)), достаточно детерминированного member. Если же требуется найти общий элемент двух заданных списков (разметка (o, i, i)), нужна недетерминированная версия.
21
Рассмотрение третьего типового случая использования отсечения
требует предварительного знакомства с другим предикатом управления
доказательством. Это тождественно ложный предикат «неудача» (fail).
Эффект этого предиката эквивалентен попытке доказать N=N+1, что невозможно с точки зрения логики. Назначение предиката неудача, таким
образом, заключается в том, что он форсирует процесс возврата. Комбинация “отсечение – неудача” констатирует неудачу доказательства подцели без права поиска других вариантов ее решения. Предположим, мы хотим выразить мысль, что все студенты хороши, кроме ленивых. С использованием комбинации “отсечение - неудача” она может быть формализована так:
Good_student(X):-lazy_student(X),!,fail.
Good_student(_).
Кроме комбинации «отсечение - неудача», интересен смысл второго
утверждения и сам порядок утверждений предиката. Второе утверждение
играет здесь роль своеобразной «ловушки»: если не удалось доказать, что
студент ленив, значит, он хорош. Понятно, что в данном случае фактловушка должен следовать за основным правилом. Однако в целом такая
последовательность нетипична, так как обычно, с учетом тонкостей последовательного доказательства, стремятся располагать факты перед правилами и легко доказуемые подцели в правой части правила перед более
сложно доказуемыми. Отметим, что можно сформулировать приведенное
утверждение иначе, использовав встроенный предикат not. Not преуспевает, если предикат, заданный в качестве его аргумента, недоказуем:
Good_student(X):-not(lazy_student(X)).
Предикат неудача используется также для форсирования возврата в
процессе доказательства, инициированном внутренней целью. Возвращаясь к задаче о родственных отношениях, перепишем определение внутренней цели с использованием предиката неудача:
Goal
Brother(X,Y), write(X,Y), nl, fail.
Инициированный fail процесс возврата стимулирует поиск всех возможных способов доказательства Brother(X, Y). Внелогические предикаты
write и nl при возврате не пересогласуются.
Наконец, предикат неудача можно использовать для замены рекурсии итерацией и сопутствующей экономии стека. Например, версия
утверждения
Everybody:-male(X),write(X),nl,fail.
в плане использования стека предпочтительнее, чем
22
Everybody:-male(X),write(X),nl,everybody.
Подводя итог обсуждению предикатов отсечение и неудача, еще раз
напомним о главном: способствуя повышению эффективности доказательства, эти предикаты лишают декларативной семантики утверждения,
в которых они содержатся. Существенным становится порядок подцелей в
утверждении и сам порядок утверждений, что нехарактерно для логических программ.
Примеры аксиоматизации и решения задач
Пример 6. Определение спецификационного списка изделия
Предположим, что некое техническое устройство состоит из деталей
и узлов. Деталь - это неделимая часть изделия, в то время как узел – часть
изделия, состоящая из деталей и узлов. Структура изделия, таким образом, определяется рекурсивно через понятие узла. Например, велосипед
состоит из рамы, руля и колес; колесо, в свою очередь, из обода, спиц и
т.д. Ограничивает рекурсию понятие детали. Определим понятия узла,
детали и спецификации в терминах Пролога:
domains
Object=symbol
List=object
predicates
Detail(object)
Unit(object,list)
Spec(object,list)
Subspec(list,list)
Append(list,list,list)
clauses
Unit(“велосипед”,[“рама”,”руль”,”колесо”]).
detail(“спица”).
spec(X,[X]):-detail(X).
spec(X,L):-unit(X,L1), subspec(L1,L).
Subspec([],[]).
Subspec([H|T],L):-spec(H,L1), subspec(T,L2),
append(L1,L2,L).
Append([],L,L).
Append([H|L1],L2,[H|L3]):-append(L1,L2,L3).
Среди приведенных аксиом в пояснении нуждается только Subspec.
Этот предикат устанавливает отношение между списком комплектующих
узла и эквивалентным ему списком деталей. Для нетривиального случая
эквивалентный список деталей определяется как результат слияния спецификации головного элемента списка комплектующих с подспецификацией хвоста этого списка. Обеспечив ясную и непротиворечивую модель
23
предметной области, можно не думать о том, как Пролог-машина будет
строить на ней доказательство такого запроса, как Spec(“велосипед”,L) –
построить спецификацию велосипеда.
Пример 7. Уточнение определений спецификации
Уточнение сводится к дополнению модели информацией о количестве экземпляров каждого из комплектующих в составе узла:
domains
Object=symbol
Amount=integer
Item=item(object,amount)
List=item*
Predicates
Detail(object)
Unit(object,list)
Spec(amount,object,list)
Subspec(amount,list,list)
Append(list,list,list)
Clauses
Unit(“велосипед”,
[item(“рама”,1),item(”руль”,1),item(”колесо”,2)]).
…
detail(“спица”).
…
spec(N,X,[item(X,N)]):-detail(X).
Spec(N,X,L):-unit(X,L1), subspec(N,L1,L).
Subspec(_,[],[]).
Subspec(N,[item(X,Num)|T],L):M=N*Num,spec(M,X,L1),subspec(N,T,L2),append(L1,L2,L).
Append([],L,L).
Append([H|L1],L2,[H|L3]):-append(L1,L2,L3).
Информация о количестве комплектующих в составе узла, полученная в результате выполнения запроса Spec(1,“велосипед”,L) в списке L,
может иметь следующий вид:
[…,item(“гайка”,2),…,item(“гайка”,4),…]
в том случае, если деталь «гайка» входит в состав разных узлов. Поэтому
необходимы дополнительные определения, которые бы устанавливали
отношение
между
исходным
и
«сжатым»
списками:
[…,item(“гайка”,6),…]. Эти определения приведены ниже.
domains
Press(list,list)
Press1(item,list,list,amount)
Clauses
Press([],[]).
Press([item(X,N1)|T1],[ item(X,N2)|T2]):Press1(item(X,N1),T1,L,N2),press(L,T2).
Press1(item(_,N),[],[],N).
Press1(item(X,N1),[ item(X,N2)|T],L,M):-!
24
N=N1+N2,press1(item(X,N),T,L,M).
Press1(Item,[H|T],[H|L],M):Press1(Item,T,L,M).
Здесь основной предикат Press устанавливает отношение между исходным и «сжатым» списками, используя для подсчета и удаления повторов одинаковых деталей вспомогательный предикат Press1. Предикат “отсечение” фиксирует выбор второго правила Press1, если установлено совпадение типа детали в голове исходного списка с типом удаляемой детали.
4. Качественное моделирование непрерывных
систем
«Придумать зеленое солнце легко;
трудно создать мир, в котором оно
было бы естественным…»
Дж. Толкиен
Качественное моделирование – молодое направление исследований в
области искусственного интеллекта, связанное с исследованием поведения непрерывных динамических систем посредством дискретных абстракций [2-5]. Потребность исследований в данном направлении возникла в связи с необходимостью автоматизации рассуждений о поведении
систем, параметры которых определены в порядковых шкалах или, в лучшем случае, заданы диапазоном возможных значений, а свойства в известных пределах нечувствительны к изменению формы функциональных
зависимостей между переменными системы. Речь, таким образом, идет о
более высоком уровне абстракции описания динамических систем, чем
количественное моделирование. Описание свойств в порядковых шкалах
несет меньше информации о системе, поэтому предсказанное на его основе поведение является принципиально недетерминированным. Так, в отсутствие информации о значении параметров системы второго порядка с
рассеянием энергии (рис.4.1, а) мы вправе предположить как апериодический, так и колебательный характер переходного процесса (рис. 4.1, б,в).
Учитывая сказанное, представляется перспективным использование дедуктивного механизма Пролога для вывода всех качественно различных
поведений динамической системы на основе системы аксиом, определяющих правила порождения и проверки качественных состояний. Рассмотрим аксиоматизацию качественных свойств и поведения более подробно.
Аксиомы качественного моделирования
Качественное описание состояния QS непрерывно дифференцируемой функции времени f : TS, S={(l,d)|lL, dD} отображает шкалу по25
рядковых значений времени T = {t0, (t0, t1), t1, …, (tn-1,tn), tn} на шкалу порядковых значений функции L = {l1, (l1, l2), l2, …, (lk-1,lk), lk} и ее производной D = {-, 0 +}. Здесь ti – особая точка временной шкалы, соответствующая моменту изменения качественного состояния функции, (ti-1, ti) – открытый временной интервал, lj – особое значение функции (например,
смена знака), (lj-1, lj) – открытый интервал постоянства качественного значения функции.
Качественное состояние системы функций F={f1,…,fm} есть множество качественных состояний отдельных функций, соответствующих одному качественному значению времени:
QS(F,t)={QS(f1,t), …, QS(fm,t)}.
Траектория динамической системы есть последовательность ее качественных состояний во времени
(QS(F,t0), QS(F,(t0,t1)), …, QS(F,(tn-1,tn)) QS(F,tn)).
Недетерминированное качественное поведение динамической системы ограничено действием нескольких факторов.
Во-первых, это условия непрерывности и непрерывной дифференцируемости функций времени, формализованные в виде аксиом связности
(табл. 4.1-4.2). Упомянутый в таблицах предикат Neighbour определяет
отношение соседства двух элементов в списке:
Neighbour(X, Y, L):- append(_, [X, Y|_], L).
Семантика предикатов member и append традиционна.
Эти аксиомы устанавливают зависимость между смежными по шкале времени (точка – интервал, интервал - точка) качественными состояниями функций. Например, постулируется, что функция, имеющая особое
значение и нулевую производную в точке временной шкалы, при выходе
на временной интервал может сохранить свое качественное состояние или
изменить его в любую сторону (табл. 4.1, аксиомы 1-3).
Функция, имеющая интервальное значение и положительную производную на интервале временной шкалы, при переходе в точку шкалы времени может достигнуть верхней границы интервала значений функции с
нулевой или положительной производной (табл. 4.2, аксиомы 2-3), и т.п.
Переходы, нарушающие условия непрерывности, являются запрещенными и не аксиоматизируются.
Во-вторых, на недетерминированное поведение динамической системы накладываются ограничения, обусловленные структурой. Структура динамической системы (рис. 4.1,а) P={P1, …, Pk} есть множество определенных на F предикатов структуры Pi(fm1, …, fmi), таких, что для любого качественного значения времени t Pi(fm1, …, fmi)=1, если состояния fm1,
…, fmi совместимы с определением Pi.
26
Const>0
1
P
1
P
ω
MMа)

t
б)

t
в)
Рис. 4.1. Качественная структура и поведение динамической системы:
а - структура динамической системы; б - результаты моделирования:
устойчивое поведение; в - результаты моделирования: циклическое поведение.
27
Например, если предикат Mneg(f1, f2) определяет монотонно убывающую зависимость, проходящую через 0, то положительное убывающее
состояние f1 и отрицательное возрастающее состояние f2 будут совместимы с определением Mneg ; если предикат Drv(f1, f2) определяет отношение
между функцией и ее производной, то возрастанию функции должно соответствовать положительное значение производной.
Структурные предикаты системы, в зависимости от устанавливаемой
ими зависимости между значениями функций, подразделяются на статические (предикаты суммирования, монотонные зависимости) и астатические (предикаты “производная” и “константа”). Строгое определение
условий совместимости качественных состояний со структурными предикатами дается для каждой категории отдельно. Для статических предикатов эти условия используют понятие кортежа согласованных значений.
Это кортежи особых значений функций из области определения структурного предиката и их качественных производных, удовлетворяющих
статической зависимости, которую постулирует предикат. Например,
тройки точечных значений функций (0, 0, 0), (-, 0, -) и их производных
(0, 0, 0), (-, 0, -) согласуются со статической зависимостью, устанавливаемой предикатом суммирования, пары (-, +) и (-, +) согласуются с монотонно убывающей зависимостью. Примем без доказательства, что состояния функций из области определения статического предиката совместимы с его определением, если
 текущие значения производных функций, связанных предикатом, образуют кортеж значений производных, согласованных с данным предикатом,
 отношения порядка между текущими значениями функций, связанных предикатом, и соответствующими элементами кортежей согласованных с этим предикатом значений функций образуют кортежи согласованных значений производных.
Эти общие правила совместимости для статических структурных
предикатов конкретизируют табл. 4.3-4.6. Упомянутый в определениях
предикат Order определяет отношение порядка между качественным значением функции и особой точкой ее порядковой шкалы в терминах качественных значений производных:
Order(p(L), Lm, LL, D):-!, ord(L, Lm, LL, D).
Order(i(L), Lm, LL, dec):-ord(L, LL, LL, dec), !.
Order(i(_), _, _, inc).
Ord(L, L, std):-!.
Ord(L1, L2, dec):-append(LL1, [L2|_], LL), append(_, [L1|_],
LL1),!.
Ord(_, _, inc).
Для астатических предикатов определения условия совместимости
выглядят так.
28
Качественное состояние функции совместимо с определением предиката «константа», если функция имеет нулевое значение производной.
 Качественное состояние функций f1 и f2 совместимо с определением
предиката «производная», если значение производной f1 совпадает со
знаком отношения порядка между качественным значением f2 и нулем
порядковой шкалы f2. Дополнительно предикат “производная” устанавливает правила “асимптотического” поведения: функция может
достигнуть качественного значения “плюс (минус) бесконечность”,
если ее производная имеет соответствующий знак (табл. 4.7).
В особых точках временной шкалы должно изменяться качественное
состояние по крайней мере одной функции динамической системы, иначе
полученное точечное состояние системы объявляется тупиковым, что
влечет откат доказательства. Состояние тупика фиксирует универсальный
факт equal(X,X), устанавливающий тождественность любого объекта самому себе при помощи мощных встроенных средств унификации Пролога.

29
Таблица 4.1
Точечно-интервальные переходы
№
1
2
3
4
5
6
7
8
9
10
QS(f, ti)
(lj, 0)
((lj,lj+1), 0)
(lj, +)
(lj, -)
((lj, lj+1), +)
((lj, lj+1), -)
Утверждения Пролога
QS(f,(ti, ti+1))
(lj, 0)
((lj,lj+1), +)
((lj-1, lj), -)
((lj,lj+1), 0)
((lj,lj+1), +)
((lj,lj+1), -)
((lj, lj+1), +)
((lj-1, lj), -)
((lj, lj+1), +)
((lj, lj+1), -)
PtoI(qs(p(L),std),
PtoI(qs(p(L),std),
PtoI(qs(p(L),std),
PtoI(qs(I(L),std),
PtoI(qs(I(L),std),
PtoI(qs(I(L),std),
PtoI(qs(p(L),inc),
PtoI(qs(p(L),dec),
PtoI(qs(I(L),inc),
PtoI(qs(I(L),dec),
qs(p(L), std),
qs(i(L), inc),
qs(i(L1),dec),
qs(i(L), std),
qs(i(L), inc),
qs(i(L), dec),
qs(i(L), inc),
qs(i(L1),dec),
qs(i(L), inc),
qs(i(L), dec),
LL):-member(L,LL).
LL):-neighbour(L, _, LL).
LL):-neighbour(L1, L, LL).
LL):-member (L, LL).
LL):-member (L, LL).
LL):-member (L, LL).
LL):-member (L, LL).
LL):-neighbour (L1, L, LL).
LL):-member (L, LL).
LL):-member (L, LL).
Таблица 4.2
Интервально-точечные переходы
№
1
2
3
4
5
6
7
8
9
10
30
QS(f,(ti, ti-1))
(lj, 0)
((lj, lj+1), +)
((lj, lj+1), -)
((lj, lj+1), 0)
Утверждения Пролога
QS(f, ti)
(lj, 0)
(lj+1, 0)
(lj+1, +)
((lj, lj+1),
((lj, lj+1),
(lj, 0)
(lj, -)
((lj, lj+1),
((lj, lj+1),
((lj, lj+1),
0)
+)
0)
-)
0)
ItoP(qs(p(L),
ItoP(qs(i(L),
ItoP(qs(i(L),
ItoP(qs(i(L),
ItoP(qs(i(L),
ItoP(qs(i(L),
ItoP(qs(i(L),
ItoP(qs(i(L),
ItoP(qs(i(L),
ItoP(qs(i(L),
std),
inc),
inc),
inc),
inc),
dec),
dec),
dec),
dec),
std),
qs(p(L), std),
qs(p(L1),std),
qs(p(L1),inc),
qs(i(L), std),
qs(i(L), inc),
qs(p(L), std),
qs(p(L), dec),
qs(i(L), std),
qs(i(L), dec),
qs(i(L), std),
LL):-member(L, LL).
LL):-neighbour(L, L1, LL).
LL):-neighbour(L, L1, LL).
LL):-member(L, LL).
LL):-member(L, LL).
LL):-member(L, LL).
LL):-member(L, LL).
LL):-member(L, LL).
LL):-member(L, LL).
LL):-member(L, LL).
Таблица 4.3
№
1
2
3
4
5
6
7
8
9
10
11
12
13
Условия согласования производных с предикатом “Сумматор”
Тройки согласованных значений
Утверждения Пролога
производных
(0, 0, 0)
Add_cd(std, std, std).
(0, +, +)
Add_cd(std, inc, inc).
(0, -, -)
Add_cd(std, dec, dec).
(+, 0, +)
Add_cd(inc, std, inc).
(+, +, +)
Add_cd(inc, inc, inc).
(+, -, 0)
Add_cd(inc, dec, std).
(+,-, +)
Add_cd(inc, dec, inc).
(+, -, -)
Add_cd(inc, dec, dec).
(-, 0, -)
Add_cd(dec, std, dec).
(-, +, 0)
Add_cd(dec, inc, std).
(-, +, +)
Add_cd(dec, inc, inc).
(-, 0, -)
Add_cd(dec, inc, dec).
(-, -, -)
Add_cd(dec, dec, dec).
Таблица 4.4
Условия согласования производных с предикатами “Монотонная зависимость”
№
Возрастающая зависимость
Пары согласованных значений
Утверждения Пролога
1
2
3
(0, 0)
(+, +)
(-, -)
Mpos_cd(std, std)
Mpos_cd(inc, inc)
Mpos_cd(dec, dec)
Убывающая зависимость
Пары согласованных
Утверждения Пролога
значений
(0, 0)
Mneg_cd(std, std)
(+, -)
Mneg_cd(inc, dec)
(-, +)
Mneg_cd(dec, inc)
31
Таблица 4.5
Условия совместимости качественных значений функций с предикатом “Сумматор”
№
1
2
3
4
5
6
7
8
9
10
11
12
13
Тройки согласованных значений
(0, 0, 0)
(0, +, +)
(0, -, -)
(+, 0, +)
(+, +, +)
(+, -, 0)
(+, -, +)
(+, -, -)
(-, 0, -)
(-, +, 0)
(-, +, +)
(-, 0, -)
(-, -, -)
Утверждения Пролога
% Значения совместимы, если ...
% ...список кортежей согласованных значений пуст
Add_cv(_, _, _, _, _, _, []).
% ...отношения порядка между качественными значениями
% QVx, QVy, QVz и элементами текущего кортежа
% [Cx, Cy, Cz] на шкалах LLx, LLy, LLz образуют
% тройку согласованных значений производных Dx,Dy,Dz
% и совместимы с остальными кортежами списка
Add_cv(QVx, QVy, QVz, LLx, LLy, LLz, [[Cx, Cy,
Cz]|T]):Order(QVx, Cx, LLx, Dx),
Order(QVy, Cy, LLy, Dy),
Order(QVz, Cz, LLz, Dz),
Add_cd(Dx, Dy, Dz),
Add_cv(QVx, QVy, QVz, LLx, LLy, LLz, T).
Таблица 4.6
Условия согласования качественных значений функций с предикатом “Монотонная зависимость”
Тип зависимости
Пары согласованных
Утверждения Пролога
значений
Mpos_cv ( _, _, _, _, []).
Возрастающая
(0, 0)
Mpos_cv(QVx, QVy, LLx, LLy, [[Cx, Cy]|T]):(+, +)
order(QVx, Cx, LLx, Dx),order(QVy, Cy, LLy, Dy),
Mpos_cd(Dx,Dy), Mpos_cv(QVx, QVy, QVz, LLx, LLy, LLz, T).
(-, -)
Mneg_cv ( _, _, _, _, []).
Убывающая
(0, 0)
Mneg_cv(QVx, QVy, LLx, LLy, [[Cx, Cy]|T]):(+, -)
order(QVx, Cx, LLx, Dx),order(QVy, Cy, LLy, Dy),
Mneg_cd(Dx,Dy), Mneg_cv(QVx, QVy, QVz, LLx, LLy, LLz, T).
(-, +)
32
№
1
2
3
4
5
Таблица 4.7
Условия согласования качественных значений функций с предикатом “Производная”
Функция
Производная
Утверждения Пролога
f1
df1/dt
f2
df2/dt
% знак f2(порядковое отношение с нулем шкалы)
*
+
>0
*
% совпадает с качественным значением производной f1
*
<0
*
Drv_cv1(Dx,Qvy,LLy)):*
0
=0
*
order(QVy, “0”, LLy, Dx).
%
интегрируемая функция асимптотически стремится
*
>0
*
+
% к положительному (отрицательному)бесконечному значению,
*
<0
*
-
% если ее производная положительна (отрицательна)
Drv_cv2(p(“-беск”), qs(p(Ly), _),LLy):order(p(Ly), “0”, Lly, inc).
Drv_cv2(p(“+беск”), qs(p(Ly), _),LLy):order(p(Ly), “0”, Lly, dec).
Таблица 4.8
№
1
2
3
Целевое
состояние
Устойчивость
Цикл
Выход за
границы
Определения целевых состояний
Определение
Утверждения Пролога
fF: f’=0
Quiescence([]).
Quiescence([qst(_,qs(_,std),_)|T]):- quiescence(T).
QS(F,(t0 ,t1)=
QS(F,(tn , tn+1)
fF:
QS(f)=(upper(L),+);
QS(f)=(lower(L), -)
Loop(p(_,QSS),QSSList):Append(_, [I(_, QSS0),_], QSSList), Equal(QSS, QSS0).
Divergence(QSS):Member(qst(_,qs(p(L), inc),LL), QSS),
Append(_, [L], LL);
Member(qst(_,qs(p(L), dec), [L|LL]), QSS).
33
Реализация качественного моделирования
Приведенный набор аксиом составляет основу для реализации качественного моделирования в типичной для решения интеллектуальных
задач недетерминированной стратегии «порождение – проверка». Согласно этой стратегии очередное нетупиковое состояние, порожденное из текущего при помощи аксиом связности, проверяется на совместимость с
определениями предикатов структуры и используется далее в качестве
текущего до тех пор, пока полученная траектория не будет интерпретирована как поведение системы с помощью фильтров целевого состояния.
Благодаря встроенным механизмам поиска с возвратом, процесс доказательства существования множества качественно различных траекторий
повторяется до тех пор, пока не будет обследовано все пространство состояний (рис. 4.1, б,в).
Разработанный в среде Турбо Пролог быстрый прототип интеллектуальной системы1 был впоследствии реализован средствами процедурного языка Borland C++ в среде Windows в составе комплекса средств
многоуровневого моделирования для решения задач компьютерной экспертизы [5].
5. Реализация Пролог–машины
Вычислительная модель
When I get to the bottom I go back to the
top of the slide
Where I stop, and I turn and I go for a
ride
Till I get to the bottom2 …
J.Lennon, P.McCartney. Helter Scelter
В языке Пролог запрос и множество утверждений логической программы, в контексте которой он выполняется, имеют вычислительный
смысл, т.е. вызывают определенное поведение интерпретатора (Прологмашины). Следовательно, можно говорить о вычислительной модели Пролога [6], имея в виду совокупность алгоритма, определяющего поведение
Пролог-машины, и данных, используемых ей для ведения протокола собственных действий.
Полный текст логической программы «Качественное моделирование»
приведен в приложении 1.
2
Когда я спускаюсь вниз, я возвращаюсь на вершину, останавливаюсь,
разворачиваюсь и снова еду вниз…
34
1
В соответствии с процедурной семантикой Пролога, любое утверждение Пролог-программы, включая запрос, следует интерпретировать
как процедуру, единственный положительный литерал утверждения 1 - как
заголовок процедуры, любой отрицательный литерал в теле утверждения
– как вызов процедуры, построение резольвенты – как вход в процедуру, а
возврат – как выход из нее. Рассматривая действия Пролог-машины как
применение метода резолюции к описанной процедурной модели, можно
определить следующую стратегию исполнения2: последовательный выбор вызовов (слева направо, начиная с первого вызова запроса) и активизация процедур, потенциально способных ответить на текущий вызов
(сверху вниз, начиная с первой процедуры - кандидата) в целях их решения. Попытка решения вызова P заканчивается успехом, если ответившая
на вызов процедура Q, в свою очередь, не содержит вызовов (т.е. соответствующее утверждение имеет формат факта), либо все вызовы Q решены.
Попытка решения вызова P заканчивается неудачей, если не удается найти
способную ответить на вызов процедуру Q, либо попытка решения какого-то вызова из Q заканчивается неудачей. Если это происходит, то Пролог-машина выполняет возврат после неудачи (тупиковой вершины поиска) к ближайшему вызову, имеющему неиспробованные процедурыкандидаты. Возврат к ближайшему вызову, имеющему неиспробованные
процедуры-кандидаты, происходит также и в том случае, когда решены
все вызовы запроса, в этом случае говорят о возврате после успеха.
Метафора вызова процедур позволяет представить протокол работы
Пролог-машины, регистрирующий текущее состояние управления и данных, в виде стека исполнения. Каждый элемент стека является признаком
входа в процедуру и по этой причине его называют иногда записью активизации. Однако в связи с тем, что запись активизации представляет собой
некий шаблон, хранящий существенные факты о текущем состоянии и его
связях с другими состояниями, которые Пролог-машина использует для
принятия решений относительно продолжения траектории управления и
изменения состояния данных, для нее более подходит название фрейм.
Что касается организации управления, фрейм обеспечивает Прологмашину информацией, необходимой для реализации процессов
 продолжения траектории управления,
 возврата.
Чтобы понять, какая именно существенная для продолжения траектории информация связывается с фреймом, рассмотрим попытку решения
вызова Pi путем активизации процедуры Q, которая влечет за собой создание фрейма F. Этот фрейм должен содержать информацию о том, каВ таком случае запрос – это единственная процедура без заголовка.
Последовательность вычислений, которые Пролог-машина предпринимает для доказательства запроса.
35
1
2
кой вызов следует выполнить после того, как будут успешно решены все
вызовы из Q. Очевидно, это либо указатель на следующий за Pi вызов Pi+1,
либо нулевой указатель (NIL), если Pi является последним в списке вызовов. Такой указатель называется указателем перехода П. Если указатель
перехода имеет значение NIL, то для решения вопроса о продолжении
траектории необходимо извлечь указатель перехода из фрейма, содержащего вызов Pi. Этот фрейм, породивший фрейм F путем активизации процедуры Q, называется родительским фреймом F. Указатель на родительский фрейм РФ, таким образом, также необходим для принятия решения
о продолжении траектории управления.
Свойство недетерминированности Пролог-программ приводит к
необходимости обеспечения управляющей информацией процесса возврата. Если попытка решения вызова Pi активизацией процедуры Q закончилась неудачей (или, наоборот, Pi, являющийся последним вызовом запроса, решен успешно), интерпретатор должен
 «вспомнить» ближайшую точку траектории управления, в которой
имеется по крайней мере одна неисследованная процедура-кандидат,
 идентифицировать эту процедуру.
Выполнение первого требования обеспечивает общий регистр Пролог-машины, называемый БТВ (ближайшая точка возврата). Он содержит указатель на фрейм, имеющий неисследованные процедурыкандидаты, или NIL, если такого фрейма не существует. В последнем
случае процесс возврата будет означать завершение логической программы.
Задачу идентификации процедуры-кандидата решает хранящийся во
фрейме указатель следующего кандидата СК, адресующий процедуру в
массиве кода или имеющий значение NIL, если фрейм не является точкой
возврата. Пролог-машина, выполняя возврат, находит с помощью БТВ
точку возврата F, а затем, используя указатели П и СК, сам вызов и неисследованную процедуру-кандидат. Как только эта информация извлечена
из фрейма F, сам фрейм и все его потомки, построенные при отработке
версии решения вызова Pi с помощью процедуры Q, могут быть отброшены из стека (при успешном завершении вывода прежде необходимо выдать сообщение о полученном решении). Для того, чтобы после отбрасывания F Пролог-машина не потеряла контроль за неиспользованными
возможностями, необходимо восстановить прежнее значение БТВ, существовавшее на момент создания фрейма F. Это возможно, если при создании F содержимое БТВ копируется в поле прежняя точка возврата
(ПТВ) фрейма, а регистр БТВ устанавливается на адрес F.
Алгоритм, реализующий стандартную стратегию управления, включает механизмы, необходимые для осуществления
 выбора вызова,
 выбора процедуры, отвечающей на вызов,
36
процесса возврата.
Обеспечивающая этот алгоритм информация хранится в статической
области кода программы и динамическом стеке управления. Кроме того,
некоторые характеристики состояния исполнения программы Прологмашина хранит в общих регистрах. Кроме уже упомянутого регистра
БТВ, обеспечивающего процесс возврата, существуют регистры, обеспечивающие информационную поддержку шага вызова процедуры. Это регистры адреса текущего вызова ТВ, следующего кандидата СКА и текущей процедуры ТП в массиве кода программы. Функция, которую интерпретатор выполняет с помощью этих регистров, заключается в поиске
процедуры, отвечающей на ТВ, путем сканирования кода программы,
начиная с адреса СКА. В результате выполнения поиска определяется содержимое регистра ТП и обновляется регистр СКА, причем возможны три
различных исхода поиска.
 ТП = NIL. Процедура СКА и расположенные вслед за ней в массиве
кода кандидаты не отвечают на ТВ. Для поиска подходящего для активизации выбора требуется выполнить процесс возврата. Если при
этом найдется вызов, для которого существуют неисследованные
кандидаты, регистры ТВ и СКА обновляются и повторяется процедура сканирования кода. Если такой вызов при возврате не найден, исполнение программы заканчивается.
 ТП  NIL, СКА = NIL. Детерминированный вызов процедуры. Сформированный в результате входа в процедуру фрейм не является точкой возврата, формируются только его ячейки РФ и П.
 ТП  NIL, СКА  NIL. Недетерминированный вызов. Фрейм является
точкой возврата. В нем формируются все четыре ячейки управления,
корректируется содержимое БТВ.
Кроме функций управления, заключающихся в организации вызова
процедур и возврата из них, Пролог-машина также должна обеспечивать
контроль за сопутствующими этим актам управления процессами связывания и освобождения переменных. Решение вопроса о выборе реализации такого контроля зависит от многих факторов, в том числе и от вида
программ, которые предположительно будут исполняться Прологмашиной, и поэтому неоднозначно.
Ниже рассматривается одностековая модель, основанная на естественном синхронизме процессов создания фрейма и связывания локальных переменных соответствующей процедуры. В силу того, что создание
фрейма в общем случае сопровождается связыванием переменных, в одностековой модели элементами фрейма, наряду с ячейками управления,
являются также ячейки этих переменных. В процессе выполняемого на
логической программе вывода значение ячеек может изменяться. Ячейка,
соответствующая свободной переменной, имеет значение NIL, ячейка
связанной переменной может содержать либо указатель на константу в

37
массиве кода, либо указатель на другую переменную, либо непосредственно значение константы, если размеры константы позволяют разместить ее в ячейке. Конкретный тип содержимого ячейки переменной устанавливается при помощи входящего в состав ее структуры ярлычка – тега. Имена переменных в явном виде не присутствуют во фрейме. Каждая
переменная однозначно идентифицируется по номеру фрейма и смещению соответствующей ей ячейки внутри фрейма. Вслед за получением
решения Пролог-машина должна выполнить возврат, означающий удаление фрейма, который адресуется регистром БТВ и всех следующих за ним
в стеке. Процесс возврата должен сопровождаться освобождением всех
переменных, связанных при его создании (отменой всех выходных присваиваний). Чтобы Пролог-машина была в состоянии отслеживать изменения статуса переменных при создании фрейма и использовать эту информацию при возврате, применяется еще одна управляющая структура,
называемая стеком восстановления, или следом (trace). Элементами следа
являются идентификаторы связанных переменных. Указатель на вершину
стека размещен в общем регистре ВС. Кроме того, в структуру фрейма
добавляется пятая ячейка управления – указатель следа СЛ.
При образовании фрейма Пролог-машина проверяет, не является ли
он точкой возврата. Если это так, ячейка СЛ устанавливается в значение
ВС. В любом случае, если образование фрейма приводит в результате выполнения унификации к связыванию переменной, расположенной во
фрейме, который предшествует ближайшей точке возврата, идентификатор этой переменной помещается в след и соответствующим образом изменяется регистр ВС.
При выполнении возврата
 переменные, размещенные на стеке восстановления выше позиции,
определяемой СЛ фрейма, освобождаются,
 занимаемый ими фрагмент следа отбрасывается,
 регистр ВС возвращается в состояние СЛ фрейма,
 регистр БТВ восстанавливает свое прежнее значение из ячейки ПТВ
фрейма, адресуемого ВС,
 все фреймы после БТВ и фрейм, адресуемый БТВ, удаляются из стека
управления, то есть, ВС становится равным БТВ-1.
38
Объектная реализация
Ваша способность телепатическим
путем
проецировать
свои
психопатические
фантазии
на
сознание других субъектов при полной
сохранности
силы
восприятий
поразительна…
Рэй Бредбери, Марсианские хроники
Рассмотрим объектную реализацию Пролог-машины с одностековой
архитектурой. Реализация выполнена на языке C++ стандарта ANSI C++
3, поэтому для работы с приведенными ниже фрагментами кода подойдет
любой компилятор, соответствующий данному стандарту1.
Объектно-ориентированная модель представляет собой систему взаимосвязанных сущностей - объектов, каждый из которых характеризуется
состоянием, поведением и индивидуальностью. Преимущества объектного подхода к проектированию заключаются в относительной независимости объектов, которая позволяет рассматривать большие фрагменты алгоритма логического вывода (алгоритм унификации, вызов предиката, возврат при неудаче и др.) по отдельности, не разрушая при этом целостной
картины.
В этой простой реализации интерпретатор работает только с целыми
числами и не имеет встроенной арифметики. Объектная модель Прологмашины представлена на рис. 5.1 2.
Класс TInferenceMashine и реализация алгоритма управления
Машина логического вывода реализована в виде класса
TInferenceMashine. Это «главный класс», владеющий внутренним представлением программы и стеком управления. Этот класс инкапсулирует
алгоритм управления, реализованный его методом FindSolution().
Работа алгоритма начинается с проверки, было ли найдено какоелибо решение ранее. Если это так (FirstSearch=0), происходит откат от
этого решения. Отсутствие такой проверки вызвало бы либо попытку удаления первого фрейма во время первого поиска, и соответственно немедленное неудачное завершение алгоритма, либо зацикливание алгоритма
на первом найденном решении. Затем признак неудачного останова алгоритма (Terminated) устанавливается в 0 (логическая ложь). В состояние
логической истины его может установить функция отката (BackTracking)
при попытке отката выше вершины стека.
Например, Borland C++ 4.5 и выше, Visual C++ 5.0 и выше
В диаграмме использована нотация объектных моделей, предложенная в
[7].
39
1
2
TInferenceMashine
TProcList
TProcID
TTrace
TClause
TViewArea
TFrame
TProcCall
TParameter
TFrameVar
Отношение агрегирования
(владения) один – к
одному
Отношение агрегирования
(владения) один – ко
многим
Отношение
наследования
Рис. 5.1 Объектная модель Пролог-машины








40
После инициализации начинается основной цикл алгоритма.
Смысл происходящего в нем следующий:
Выбирается очередной вызов предиката.
Осуществляется вызов(метод UnifyAndCall()),
Если вызов удачен (UnifyAndCall() породил новый фрейм), новый
фрейм добавляется к стеку и становится текущим.
В случае неудачного вызова происходит откат (метод BackTracking())
к ближайшей точке возврата.
Выбор вызова осуществляется по следующему алгоритму:
Если у текущего фрейма указатель на первую подцель предиката, вызов которого породил данный фрейм (CallList) ненулевой, то выбирается этот вызов.
Если CallList=0, то для вызова используется указатель перехода (ExitPoint).
Если ExitPoint=0 (предикат, породивший родительский фрейм, доказан), то используется точка выхода родительского фрейма. Если и там
точка выхода нулевая, используется точка выхода прародителя и т.д.
если ненулевых указателей перехода больше не найдено (поиск дошел до корня), это значит, что цель доказана и можно возвращать результат.
В данной реализации машины логического вывода в явном виде
представлен только регистр вершины стека – ВС; остальные (БТВ, ТВ,
ТП, СКА) представлены неявно. Содержимое этих регистров восстанавливается в результате несложных вычислительных манипуляций. Чтобы в
случае бесконечной рекурсии Пролог-машина не исчерпала всю свободную память, что может привести к краху системы, отслеживается (и ограничивается) глубина поиска (CurrDepth).
Поиск процедуры, отвечающей на текущий вызов, и собственно вызов осуществляет метод UnifyAndCall. В качестве параметра он принимает родительский фрейм и вызов, который необходимо осуществить (текущий вызов). В роли регистра ТВ выступает параметр ccp, вместо регистров ТП и СКА используется поле вершины стека Current->NextProc,
соответствующее полю фрейма СК. При необходимости (вызов осуществляется впервые, и к нему еще не было отката) Current->NextProc инициализируется первым кандидатом на вызов из массива внутреннего представления программы. Далее в цикле идет проверка, отвечает ли данный
кандидат на данный вызов (ccp->Execute()). Если это так, ccp->Execute()
порождает новый фрейм, который и возвращается в качестве результата.
Этот фрейм, как было сказано выше, помещается в стек непосредственно
после вершины стека (Current). Таким образом, указатель на следующего
кандидата находится во фрейме, непосредственно предшествующем
фрейму, порожденному данным вызовом (это позволяет сразу удалять
данный фрейм при откате, даже если он является точкой возврата). Далее
производится выбор следующего кандидата (обычная итерация по списку). Если кандидатов на вызов не осталось и новый фрейм не порожден,
функция возвращает 0, что свидетельствует о неудаче и влечет за собой
откат.
Метод возврата при неудаче - BackTracking() имеет довольно простой вид: из стека удаляются фреймы, начиная с вершины. Этот процесс
прекращается, как только будет найден фрейм с ненулевым указателем на
следующего кандидата (NextProc). Алгоритм вывода сможет продолжить
поиск нового решения с этого фрейма. При попытке удалить корневой
фрейм, метод устанавливает признак неудачного завершения в 1 (истина)
и завершается.
Внутреннее представление программы
Исходный текст определений преобразуется транслятором в структуру данных, удобную для осуществления с ее помощью логического вывода. Эта структура реализована в виде класса TProcList. Структура эта
представляет собой массив указателей. Каждый такой указатель соответствует предикату в исходном тексте программы и указывает на первое (по
тексту определений) высказывание для данного предиката. Высказывание
реализовано в виде класса TClause. Этот класс является простым контейнером, и его поведение связано исключительно с контейнерными функци41
ями. Итак, TClause содержит список последующих высказываний, список
подцелей и область видимости переменных. Кроме того, TClause содержит список формальных параметров. Область видимости переменных
реализована в виде класса TViewArea и содержит все параметры - переменные и константы, используемые в данном предложении в единичном
экземпляре. Параметры-переменные хранятся отдельно от констант для
удобства подсчета, поскольку при создании фрейма на каждую переменную выделяется память, а на константу – нет. Параметр реализован в виде
структуры TParameter. Он имеет значение и тег, показывающий как это
значение использовать (то есть определяет, переменная это или
константа). Список формальных параметров TClause содержит указатели
на параметры из области видимости. Здесь, естественно, могут быть
ссылки на один и тот же параметр несколько раз. Вызов процедуры (или
условие) реализован в виде класса TProcCall. Объекты этого класса можно
объединять в списки. TProcCall содержит список фактических параметров
(массив указателей на параметры из области видимости предложения,
которому принадлежит данный вызов). Наличие общих свойств (списка
параметров и имени) у TClause и TProcCall позволяет унаследовать их от
одного класса (TProcID), что упрощает транслятор исходного текста.
Класс TProcCall обладает сложным поведением. Метод TProcCall::Execute(TClause* predicate, TFrame* State) реализует попытку вызова
процедуры predicate в контексте фрейма State. Метод производит попарную унификацию формальных и фактических параметров. Точнее, в методе сначала производится предварительный отсев заведомо неподходящих кандидатов (когда ясно, что кандидат не подходит, без присваивания
значений переменным). Например, заведомо не подходит кандидат, у которого в качестве параметра есть константа, не равная соответствующему
значению фактического параметра (значению соответствующей связанной
переменной или константы). Затем производится интерпретация тегов
соответствующих параметров и определение типа унификации (переменная – переменная, переменная - константа). Для этих типов унификации
осуществляется вызов соответствующих методов TFrame, которые и производят собственно унификацию соответствующих параметров. В случае
успеха метод порождает новый фрейм, соответствующий входу в
predicate.
Объектная реализация фрейма: класс TFrame
Фрейм реализован в виде класса TFrame. Таким образом, TFrame инкапсулирует регистры фрейма, вектор переменных и позволяет создавать
из фреймов связный список. TFrame не допускает присваивания своим
переменным значений иначе как посредством унификации (см. метод
резолюций). Для этого он имеет перегруженный метод UnifyVar для унификации переменной и константы и для унификации двух переменных.
Следует отметить некоторые важные моменты, связанные с унификацией
42
двух переменных. В случае унификации двух несвязанных переменных
одна из них получает значение указателя на другую. В дальнейшем, если
требуется унификация переменной-указателя, производится итерация по
цепочке указателей до ячейки переменной, содержащей ее фактическое
значение. С этой ячейкой и производятся дальнейшие манипуляции. Такой подход позволяет по возможности избегать цепочек указателей длиннее 1, хотя и не гарантирует полностью их отсутствия. Если при унификации имело место присваивание значения, информация об этом (объект
TTrace) добавляется в след фрейма, для которого был вызван метод
UnifyVar. Таким образом, поскольку при унификации всегда вызываются
методы фрейма, порождаемого в данный момент, гарантируется, что при
удалении этого фрейма отменяются все присваивания, произведенные при
его создании.
Класс TTrace и реализация следа
Поскольку каждый фрейм должен знать о присваиваниях, совершенных при его создании, стек следа в данной реализации «размазан» по стеку управления. Фрейм содержит указатель на список объектов класса
TTrace. Объект класса TTrace содержит указатель на ячейку вектора переменных, для которой производилось присваивание. Функция объекта
TTrace состоит в том, чтобы при своем уничтожении установить переменную, указатель на которую он содержит в несвязанное состояние. Эту
функцию выполняет деструктор. Кроме того, деструктор рекурсивно удаляет следующий объект TTrace. Таким образом, чтобы удалить список
объектов TTrace, следует просто удалить его первый элемент.
43
Список литературы
1.
2.
3.
4.
5.
6.
7.
44
Ин Ц., Соломон Д. Использование ТурбоПролога.: Пер. с англ.- М.:
Мир, 1993.- 608 с.
Forbus Kenneth D. Commonsense Physics: A Review. Ann Rev. Comp.
Science 1988, 3.- p. 197-232
Kuipers, B. Qualitative Simulation. Artificial Intelligence 29 (1986).-p.
289-338
Пантелеев Е.Р., Косоруков А.Л. Логическое моделирование динамических систем // Изв. Вузов. Энергетика, 1998 №2 с. 67-72
Методы и инструментальные средства многоуровневого моделирования динамических систем для решения задач компьютерной экспертизы. В.Н. Нуждин, Е.Р. Пантелеев и др. Отчет о НИР, Иваново, 1994,
43 с.
Хоггер К. Введение в логическое программирование: Пер. с англ.М.:Мир, 1988.-348 с.
Object-Oriented Modeling and Design. James Rumbaugh, Michael Blaha,
William Premerlani, Frederick Eddy, William Lorensen/Prentice Hall New
Jersey 1991. 500 p.
Приложения
Реализация определений качественного
моделирования на языке Пролог
domains
lm = symbol
/* точка поpядковой шкалы*
lmlist = lm*
/* поpядковая шкала*/
dir = inc; dec; std
/* значение пpоизводной*/
qv = point(lm); interval(lm)/* значение*/
qs = qs(qv, dir)
/* качественное состояние*/
f = symbol
/* имя функции в стpуктуpе*/
qsf = qsf(f, qs, lmlist)
/* кач. сост. функции */
qss = qsf*
/* кач. сост. системы функций */
qsse = i(qss); p(qss)
/* pасшиpенное качеcтвенное
состояние */
qsslist = qsse*
/* тpаектоpия системы */
tuples = lm*
/* пары, тройки согласованных
значений */
tupleslist = tuples* /* список согласованных значений */
spr =
/* стpуктуpный пpедикат */
sdrv(f, f);
/* пеpвая пpоизводная */
smp(f, f, tupleslist); /* возpастающая функция */
smn(f, f, tupleslist); /* убывающая функция */
sadd(f, f, f, tupleslist);/* сумматоp */
const(f)
/* константа */
spl = spr*
/* кач. опpеделение стpуктуpы ДУ */
predicates
member(qsse, qsslist)
member(lm, lmlist)
member(qsf, qss)
/* пpинадлежность спискам */
append(lmlist, lmlist, lmlist)
/* сложение двух списков*/
append(qsslist, qsslist, qsslist)
neighbour(lm, lm, lmlist)
order(qv, lm, lmlist, dir)
ord(lm, lm, lmlist, dir)
PtoI(qs, qs, lmlist)
ItoP(qs, qs, lmlist)
/* соседи по поpядкоой шкале */
/* отношения поpядка */
/* отношения поpядка */
/* пеpеход из точки на интеpвал */
/* пеpеход c интеpвала в точку */
Mpos_cd(dir, dir)
/* совместимость с возpастающей ф-ей */
Mpos_cv(qv, qv, lmlist, lmlist, tupleslist)
Mneg_cd(dir, dir)
/* совместимость с убывающей ф-ей */
Mneg_cv(qv, qv, lmlist, lmlist, tupleslist)
Add_cd(dir, dir, dir)
/* совместимость с сумматоpом */
Add_cv(qv, qv, qv, lmlist, lmlist, lmlist, tupleslist)
45
drv_c1(dir,qv,lmlist)
drv_c2(qv, qs, lmlist)
check(qss, spl)
check1(qss, spr)
predict_p(qss, qss)
predict_i(qss, qss)
/* совместимость с пpоизводной */
/* пpовеpка совместимости КСС со
писком стpуктуpных пpедикатов */
/* пpовеpка совместимости КСС со
стpуктуpным пpедикатом */
/* пpедсказание из точки */
/* пpедсказание с интеpвала */
same_qss(qss, qss)
/* тупик */
endpoint(qss)
/* конечная точка */
quiescence(qss)
/* установившееся состояние */
loop(qsse, qsslist)
/* цикл */
predict(qsse, spl, qsslist) /* пpедсказание (целевой пpедикат)
*/
write_list(qsslist) /* pаспечатка списка */
writelist(qss)
clauses
member(Lm, [Lm|_]).
member(Lm, [_|T]) :- member(Lm, T).
append([], X, X).
append(X, [], X).
append([H|T1], L2, [H|T3]) :- append(T1, L2, T3).
neighbour(L1, L2, [L1, L2|_]).
neighbour(L1, L2, [_|T]) :- neighbour(L1, L2, T).
ord(L1, L2, LL, dec) :- append(LL1, [L2|_], LL), member(L1,
LL1).
ord(L1, L1, LL, std) :- member(L1, LL).
ord(L1, L2, LL, inc) :- append(LL1, [L1|_], LL), member(L2,
LL1).
order(interval(L), L,_ , inc).
order(interval(L), Lm, LL, Dir) :- L <> Lm, ord(L, Lm, LL, Dir).
order(point(L), Lm, LL, Dir) :- ord(L, Lm, LL, Dir).
PtoI(qs(point(L), std), qs(point(L), std), _).
PtoI(qs(point(L), std), qs(interval(L), inc), LL) :- neighbour
(L, _, LL).
PtoI(qs(point(L), std), qs(interval(L1), dec), LL) :- neighbour
(L1, L, LL).
PtoI(qs(point(L), inc), qs(interval(L), inc), LL) :- neighbour
(L, _, LL).
PtoI(qs(point(L), dec), qs(interval(L1), dec), LL) :- neighbour
(L1, L, LL).
PtoI(qs(interval(L), std), qs(interval(L), std), _).
PtoI(qs(interval(L), std), qs(interval(L), inc), _).
PtoI(qs(interval(L), std), qs(interval(L), dec), _).
46
PtoI(qs(interval(L), inc), qs(interval(L), inc), _).
PtoI(qs(interval(L), dec), qs(interval(L), dec), _).
ItoP(qs(point(L), std), qs(point(L), std), _).
ItoP(qs(interval(L), inc), qs(point(L1), std), LL ) :neighbour(L, L1, LL).
ItoP(qs(interval(L), inc), qs(point(L1), inc), LL ) :neighbour(L, L1, LL).
ItoP(qs(interval(L), inc), qs(interval(L), inc), _).
ItoP(qs(interval(L), inc), qs(interval(L), std), _).
ItoP(qs(interval(L),
ItoP(qs(interval(L),
ItoP(qs(interval(L),
ItoP(qs(interval(L),
ItoP(qs(interval(L),
dec),
dec),
dec),
dec),
std),
qs(interval(L), dec), _).
qs(interval(L), std), _).
qs(point(L), std), _).
qs(point(L), dec), _).
qs(interval(L), std), _).
Mpos_cd(X, X).
Mpos_cv(_, _, _, _, []).
Mpos_cv(QVx, QVy, LLx, LLy, [[X, Y]|T]) :order(QVx, X, LLx, Dx), order(QVy, Y, LLy, Dy),
Mpos_cd(Dx, Dy), Mpos_cv(QVx, QVy, LLx, LLy, T).
Mneg_cd(dec, inc).
Mneg_cd(inc, dec).
Mneg_cd(std, std).
Mneg_cv(_, _, _, _, []).
Mneg_cv(QVx, QVy, LLx, LLy, [[X, Y]|T]) :order(QVx, X, LLx, Dx), order(QVy, Y, LLy, Dy),
Mneg_cd(Dx, Dy), Mneg_cv(QVx, QVy, LLx, LLy, T).
Add_cd(std, X, X).
Add_cd(X, std, X).
Add_cd(inc, inc, inc).
Add_cd(inc, dec, inc).
Add_cd(inc, dec, dec).
Add_cd(inc, dec, std).
Add_cd(dec, inc, dec).
Add_cd(dec, inc, inc).
Add_cd(dec, inc, std).
Add_cd(dec, dec, dec).
Add_cv(_, _, _, _, _, _, []).
Add_cv(QVx, QVy, QVz, LLx, LLy, LLz, [[X, Y, Z]|T]) :order(QVx, X, LLx, Dx), order(QVy, Y, LLy, Dy), order(QVz, Z,
LLz, Dz),
Add_cd(Dx, Dy, Dz), Add_cv(QVx, QVy, QVz, LLx, LLy, LLz, T).
drv_c1(Dx,QVy,LLy):-order(QVy,"0",LLy,Dx).
drv_c2(point("+беск."), qs(Qv, std), LL) :order(Qv, "0", LL, inc), !.
drv_c2(point("-беск."), qs(Qv, std), LL) :-
47
order(Qv, "0", LL, dec), !.
drv_c2 (point("+беск."), _, _) :- !, fail.
drv_c2 (point("-беск."), _, _) :- !, fail.
drv_c2 (_, _, _).
check(_, []) :- !.
check(Qss, [H|T]) :- check1(Qss, H), check(Qss, T).
check1(S, sdrv(F, G)) :- member(qsf(F, qs(IP, B), LM), S),
member(qsf(G, qs(IP1, D), _), S), /*order(IP, "0", LM, D),*/
drv_c1(D,IP,LM),
drv_c2(IP1, qs(IP, B), LM).
check1(S, const(F)) :- member(qsf(F, qs(_, std), _), S).
check1(S, smp(F, G, TL)) :- member(qsf(F, qs(IP, D), LM), S),
member(qsf(G, qs(IP1, D1), LM1), S), Mpos_cv(IP, IP1, LM, LM1,
TL),
Mpos_cd(D, D1).
check1(S, smn(F, G, TL)) :- member(qsf(F, qs(IP, D), LM), S),
member(qsf(G, qs(IP1, D1), LM1), S), Mneg_cv(IP, IP1, LM, LM1,
TL),
Mneg_cd(D, D1).
check1(S, sadd(F, G, H, TL)) :- member(qsf(F, qs(IP, D), LM),
S),
member(qsf(G, qs(IP1, D1), LM1), S),
member(qsf(H, qs(IP2, D2), LM2), S),
Add_cv(IP, IP1, IP2, LM, LM1, LM2, TL),
Add_cd(D, D1, D2).
predict_p([], []).
predict_p([qsf(N, B, L)|T], [qsf(N, C, L)|T1]) :PtoI(B, C, L), predict_p(T, T1).
predict_i([], []).
predict_i([qsf(N, B, L)|T], [qsf(N, C, L)|T1]) :ItoP(B, C, L), predict_i(T, T1).
same_qss(X, X).
endpoint(H) :- member(qsf(_, qs(point("+беск."), inc), _), H).
endpoint(H) :- member(qsf(_, qs(point("-беск."), dec), _), H).
quiescence([]).
quiescence([qsf(_, qs(_, std), _)|H]) :- quiescence(H).
loop(X, L) :- member(X, L).
writelist([]) :- nl,readchar(_).
writelist([H|T]) :- write(H), nl, writelist(T).
write_list([]) :- nl.
write_list([p(H)|T]) :writelist(H).
write_list([i(H)|T]) :writelist(H).
48
write_list(T),write("point\n"),
write_list(T),write("interval\n"),
predict(i(QSS), L, T1) :loop(i(QSS), T1),
write(" Цикл! "),
nl,
write_list([i(QSS)|T1]), nl, !;
predict_i(QSS, QSS1),
not(same_qss(QSS, QSS1)),
check(QSS1, L),
predict(p(QSS1), L, [i(QSS)|T1]).
predict(p(QSS), L, T1) :endpoint(QSS),
write(" Выход за границу интервала ! "), nl,
write_list([p(QSS)|T1]), nl, !;
quiescence(QSS),
write(" Установившийся режим! "), nl,
write_list([p(QSS)|T1]), nl, !;
predict_p(QSS, QSS1),
not(same_qss(QSS, QSS1)),
check(QSS1, L),
predict(i(QSS1), L, [p(QSS)|T1]).
GOAL
write ("HeИдеальный осциллятор : "), nl,
predict(p([ qsf(c, qs(interval("0"), std), ["-беск.", "0",
"+беск."]),
qsf(a, qs(interval("0"), dec), ["-беск.", "0",
"+беск."]),
qsf(v, qs(point("0"), inc), ["-беск.", "0", "+беск."]),
qsf(s, qs(point("0"), std), ["-беск.", "0", "+беск."]),
qsf(v1, qs(point("0"), dec), ["-беск.", "0",
"+беск."]),
qsf(s1, qs(point("0"), std), ["-беск.", "0",
"+беск."]),
qsf(a1, qs(point("0"), dec), ["-беск.", "0",
"+беск."])]),
[sdrv(v, s),
sdrv(a, v),
smn(s1, s, [
["-беск.", "+беск."],
["0", "0"],
["+беск.", "-беск."]
]),
smn(v1, v, [
["-беск.", "+беск."],
["0", "0"],
["+беск.", "-беск."]
]),
sadd(v1,s1,a1, [["0", "0", "0"],
["0", "-беск.", "-беск."],
["+беск.", "0", "+беск."],
["+беск.", "-беск.", "+беск."],
["+беск.", "-беск.", "0"],
["+беск.", "-беск.", "-беск."]]),
49
sadd(c,a1,a, [["0", "0", "0"],
["0", "-беск.", "-беск."],
["+беск.", "0", "+беск."],
["+беск.", "-беск.", "+беск."],
["+беск.", "-беск.", "0"],
["+беск.", "-беск.", "-беск."]]),
const(c)],
[]).
Объектная реализация Пролог-машины
Классы объектной модели
Наименование
класса
TProcList
Защищенное
свойство
list
Size
Count
Delta
Роль класса в объектной модели
Внутреннее представление программы
Тип
Описание
Тип
Вектор предикатов
Размер вектора
Количество предикатов
Размер приращения вектора при переполнении
Описание
~TProcList()
Compile (char*
text)
int
Конструктор внутреннего представления
Деструктор
Функция интерпретации текста
operator[] (int x)
TClause*
Открытый метод
TProcList();
TProcID
Открытое
свойство
Name
ParamList
50
TClause**
int
int
int
Оператор доступа к вектору предикатов
Суперкласс для высказывания и вызова предиката
Тип
Описание
char [32]
TArrayTemplate
<TParameter*>
Имя предиката или вызова
Список параметров
Открытый метод
TProcID
(const
char*
name="")
Тип
Конструирует объект TProcID с заданным именем
Деструктор
~TProcID()
operator== (const
TProcID& pi)
TClause
Открытое
свойство
CallList;
Next
ViewArea;
Открытый метод
int
Тип
Класс предиката
Описание
Тип
void
Конструктор, создает высказывание с
заданным именем
Деструктор, удаляет список подцелей
и следующих кандидатов
Добавление подцели к высказыванию
TClause*
TViewArea
~TClause()
Открытый метод
TProcCall (const
char* name=0,
int procId = undefined)
~TProcCall()
Оператор сравнения по имени
Указатель на первый элемент в списке
подцелей
Указатель на следующего кандидата
на вызов
Область видимости переменных
Описание
TProcCall*
TClause (const
char* name)
Add (TProcCall*
call)
TProcCall
Открытое
свойство
Next;
ProcId;
Описание
Тип
TProcCall*
int
Тип
Класс вызова предиката
Описание
Следующая подцель в высказывании
Идентификатор вызываемого предиката
Описание
Конструктор. создает объект вызова с
заданными именем и идентификатором процедуры
Деструктор. Рекурсивно удаляет следующую подцель. Таким образом,
весь список вызовов предиката удаляется при удалении его начала
51
Вызов предиката. Производит унификацию всех фактических и формальных параметров предиката.
Predicate – вызываемый предикат,
State – фрейм, в контексте которого
осуществляется вызов, возвращает
указатель на новый фрейм или NULL
в случае неудачи
int
Проверка возможности унификации
переменных. Служит для отсева заведомо неподходящих кандидатов на
вызов.
pPred – указатель на заголовок предиката, State – указатель на фрейм, из
которого вызывается предикат. Возвращает 1 в случае успеха и 0 в случае
неудачи
Класс параметра (формального или фактического)
Тип
Описание
virtual Execute
(TClause* predicate, TFrame*
State)
TFrame*
CanUnify
(TClause* pPred,
TFrame* State)
TParameter
Открытое
свойство
Name
char[32]
Имя параметра
valType;
TValType
Value
int
Тип значения. Показывает, каким
образом
использовать
значение,
содержащееся в Value.
Принимает следующие значения:
var – параметр – переменная (Value
содержит индекс переменной в векторе переменных фрейма), constant –
параметр – константа (Value содержит
значение константы)
anonimous – анонимная переменная
(Value не используется)
Значение параметра
52
Открытый метод
TParameter
(const
char*
name="",
TValType vtype
= constant, int
value = 0)
TParameter
(const
TParameter& p)
operator = (const
TParameter& p)
operator==(const
TParameter& p)
TViewArea
Защищенное
свойство
Variables
Constants
Открытый метод
TViewArea()
~TViewArea()
Тип
Описание
Конструктор создания параметра с
заданным именем, типом значения и
значением, он же – конструктор по
умолчанию.
Конструктор копирования
TParameter&
int
Оператор присваивания
Оператор сравнения параметров по
имени
Класс области видимости переменных
Тип
Описание
TArrayTemplate
<TParameter*>
TArrayTemplate
<TParameter*>
Тип
GetVariable
(LPCSTR
paramName)
TParameter*
GetConstant (int
constVal)
TParameter*
GetAnonimous()
TParameter*
GetVarCount()
int
Переменные области видимости
Константы области видимости
Описание
Конструктор
Деструктор, физически удаляет переменные, хранимые в области видимости
По требованию интерпретатора исходного текста возвращает указатель
на параметр – переменную с заданным
именем
По требованию интерпретатора исходного текста возвращает указатель
на параметр – константу с заданным
значением
По требованию интерпретатора исходного текста возвращает указатель
на анонимную переменную
Возвращает количество переменных
53
TFrame
Защищенное
свойство
varState
TFrameVar*
Trace
TTrace*
Тип
Открытое
свойство
isNew
Тип
bool
ParentState
PrevState
ExitPoint
CallList
NextProc
TFrame*
TFrame*
TProcCall*
TProcCall*
TClause*
Открытый метод
TFrame(TFrame*
parentState,
TProcCall*
callList,
TProcCall*
exitPoint,
int varCount)
~TFrame()
Тип
Состояние
переменных
области
видимости
Стек восстановления, информация обо
всех присваиваниях, совершенных
при создании данного фрейма
Описание
Признак
необходимости
инициализации СК
Родительский фрейм
Предыдущий фрейм
точка выхода
Следующий вызов
Следующий кандидат (на вызов
предиката, породившего фрейм,
непосредственно следующий за
данным)
Описание
Конструктор. Создает новый фрейм с
заданными родительским состоянием,
следующей подцелью, точкой выхода.
Размещает в памяти вектор переменных размера varCount
GetVarValue (
int iVar,
long& Value)
bool
UnifyVar (
int iVar, TFrame*
pOwner, long
Value,
)
bool
54
Класс фрейма
Описание
Деструктор удаляет след данного
фрейма и затем вектор переменных.
Таким образом, все присваивания из
данного фрейма отменяются
Определение значения переменной
Value с индексом iVar в векторе
переменных фрейма. Если переменная
свободна, метод возвращает false
Метод унификации переменной с индексом iVar и принадлежащей фрейму
pOwner и константы со значением
Value при создании данного фрейма
UnifyVar (
int iVar,
int iDestVar,
TFrame*
pOwner);
Push
(TFrame* prev)
TFrameVar
Метод унификации переменной данного (вновь создаваемого) фрейма с
индексом iVar и переменной другого
фрейма, pOwner, с индексом iDestVar.
В случае неудачи возвращает false
Добавление фрейма в стек prev –
предыдущий фрейм, вершина стека
Класс состояния переменной
bool
void
Открытое
свойство
State
EVarState
Value
TVarValue
Открытый метод
TFrameVar(
EVarState
_State=undefined
,long _Value=0)
operator=(const
TFrameVar& vs)
TTrace
Защищенное
свойство
next
var
Открытый метод
TTrace(
TFrameVar*
_var)
Тип
Тип
Описание
Состояние переменной. Принимает
следующие значения:
bound – связанная переменная
ptr – указатель
undefined – несвязанная переменная
значение переменной. Тип TvarValue
– объединение. TVarValue выступает
либо как целочисленное значение,
либо как указатель на TFrameVar
Описание
Конструктор
TFrameVar&
Тип
TTrace*
TFrameVar*
Тип
Оператор присваивания
Элемент стека следа
Описание
Следующий след
Указатель на переменную, значение
которой следует вернуть при откате в
состояние неопределенности
Описание
Конструктор. Создает новый след,
содержащий информацию о присваивании переменной _var значения
55
~TTrace()
Push(TTrace* t)
TInferenceMashi
ne
Защищенное
свойство
Depth
CurrDepth
Terminated
FirstSearch;
Деструктор – возвращает переменной
var неопределенное состояние и
удаляет оставшийся список следов.
Таким образом, удалив начало стека,
мы удаляем весь стек
TTrace*
добавление следа в стек
Машина вывода, инкапсулирует алгоритм управления
Тип
int
Максимальная глубина стека
int
int
int
Текущая глубина стека
Признак прерывания поиска
Признак первого поиска. показывает,
нужно ли совершить откат перед
началом поиска очередного решения
Внутреннее представление программы
Текущий фрейм, вершина стека (ВС).
ProcList
Current
TProcList
TFrame*
Top
TFrame*
Открытый метод
TInferenceMashi
ne(
int searchDepth,
char*
programText)
~TInferenceMash
ine();
Описание
Тип
Первый элемент стека, фрейм, порожденный целевым утверждением
Описание
Конструктор. Создает машину вывода,
используя в качестве параметров максимальную глубину стека и текст
определений (который компилируется
ProcList во внутреннее представление)
Деструктор. При необходимости уничтожает стек
FindSolution()
TFrame*
GetTop()
TFrame*
GetGoalPredicate
()
56
TClause*
Функция
поиска
решений.
Производит поиск текущего вызова,
его вызов, в случае неудачного вызова
– откат. Возвращает ненулевой
указатель, если найдено решение или
NULL в случае неудачи
Возвращает указатель на первый
элемент стека (где находится фрейм
входа в целевой предикат)
Возвращает указатель на целевое
высказывание
UnifyAndCall(
TFrame* State,
TProcCall* ccp);
TFrame*
Функция вызова предиката ccp из
фрейма State. В случае удачи
возвращает указатель на новый
фрейм. в противном случае – NULL,
что влечет за собой откат
BackTracking();
void
Функция
отката
при
неудаче.
Производит
последовательное
удаление фреймов из стека, пока не
достигнет фрейма с ненулевым
NextProc, что означает достижение
БТВ, или первого фрейма в стеке, что
означает невозможность дальнейшего
поиска решений
Исходный текст
Заголовочный файл “ engine.h”
#if!defined(__engine _h)
#define __engine_h
#include "commtpl.h"
#include <windows.h>
#include <windowsx.h>
#pragma hdrstop
// Перечисление состояний переменной: определена, указатель,
неопределена
enum EVarState{bound,ptr,undefined=-32767};
// Класс параметра предиката
class TParameter
{
public:
//Перечисление типа параметра:
//переменная, константа, анонимная переменная
enum TValType{var,constant,anonimous};
// Имя параметра
char Name[32];
// Тип значения
TValType valType;
// Значение: для переменной - индекс в векторе переменных
//фрейма,
// для константы - непосредственное значение
int Value;
// конструктор создания параметра, он же - конструктор
// по умолчанию
TParameter(const char* name="",TValType vtype=constant,int
57
value=0):
valType(vtype), Value(value){strcpy(Name,name);}
// конструктор копирования
TParameter(const TParameter& p):
valType(p.valType), Value(p.Value){strcpy(Name,p.Name);}
TParameter& operator=(const TParameter& p)
{
valType=p.valType;
Value=p.Value;
strcpy(Name,p.Name);
return *this;
}
int operator==(const TParameter& p){return ! strcmp(p.Name,Name);}
};
//------------------------------------------------------------// Список параметров, реализован в виде вектора
class TViewArea
{
protected:
TArrayTemplate<TParameter*> Variables;
TArrayTemplate<TParameter*> Constants;
public:
TViewArea():Variables(0,2),Constants(0,2){}
virtual ~TViewArea();
TParameter* GetVariable(LPCSTR paramName);
TParameter* GetConstant(int constVal);
TParameter* GetAnonimous();
int GetVarCount(){return Variables.GetCount();}
};
//------------------------------------------------------------class TFrameVar;
union TVarValue
{
TVarValue(long val):Integer(val){}
TVarValue(TFrameVar far* fv):Pointer(fv){}
long operator=(TVarValue VV){return Integer=VV.Integer;}
int operator ==(TVarValue VV){return Integer==VV.Integer;}
int operator !=(TVarValue VV){return Integer!=VV.Integer;}
long Integer;
TFrameVar far* Pointer;
};
//------------------------------------------------------------// состояние переменной
class TFrameVar
{
public:
// состояние (см. перечисление состояний)
EVarState State;
// значение
TVarValue Value;
TFrameVar(EVarState _State=undefined,long
_Value=0):State(_State),Value(_Value){};
TFrameVar& operator=(const TFrameVar& vs)
{
58
State=vs.State;
Value.Integer=vs.Value.Integer;
return *this;
}
};
//------------------------------------------------------------// Суперкласс для заголовков и вызовов предикатов
class TProcID
{
public:
// Имя
char Name[32];
TArrayTemplate<TParameter*> ParamList;
TProcID(const char* name="");
virtual ~TProcID(){};
operator==(const TProcID& pi){return strcmp(pi.Name,Name)==0;}
};
class TClause;
class TProcList;
class TFrame;
//-----------------------------------------------------------// класс вызова предиката
class TProcCall:public TProcID
{
public:
// следующий кандидат
TProcCall* Next;
// идентификатор вызываемого предиката
int ProcId;
// список указателей на переменные области видимости
public:
TProcCall(const char* name=0,int procId=undefined):TProcID(name),
ProcId(procId),Next(0){}
virtual ~TProcCall(){if(Next)delete Next;}
// Вызов предиката. p - вызываемый предикат, State - фрейм, в
контексте которого
// осуществляется вызов, возвращает указатель на новый фрейм или
NULL в случае
// неудачи. Меняет состояние p на следующего кандидата
virtual TFrame* Execute(TClause* predicate, TFrame* State);
protected:
// Проверка возможности унификации переменных. Служит для
// отсева заведомо неподходящих кандидатов на вызов
// pPred - указатель на заголовок предиката,
// State - указатель на фрейм,из которого вызывается предикат
int CanUnify(TClause* pPred,TFrame* State);
};
//----------------------------------------------------------// класс заголовка предиката
class TClause : public TProcID
{
59
protected:
// указатель на последнего кандидата на вызов
TProcCall* LastCall;
public:
// список подцелей
TProcCall* CallList;
// указатель на следующего кандидата на вызов
TClause* Next;
// область видимости переменных
TViewArea ViewArea;
TClause(const char* name): TProcID(name), CallList(0),
LastCall(0), Next(0){ }
~TClause(){if(CallList)delete CallList;if(Next)delete Next;}
// добавление подцели
void Add(TProcCall* call)
{
if(!CallList){CallList=call;LastCall=call;}
else {LastCall->Next=call;LastCall=call;}
}
};
// класс исключительной ситуации для интерпретатора
class TSyntaxError
{
public:
// сообщение
char msg[255];
// строка
int Line;
// позиция
int Pos;
TSyntaxError(const char* _why,int line,int
pos):Pos(pos),Line(line){strcpy(msg,_why);}
};
// класс списка предикатов. Внутреннее представление программы
// Инкапсулирует функции интерпретатора
class TProcList
{
protected:
// список (вектор) предикатов
TClause** list;
// количество предикатов
int Count;
// размер вектора
int Size;
// размер приращения вектора при переполнении
int Delta;
public:
TProcList();
60
~TProcList();
// функция интерпретации текста
int Compile(char* text);
// функция доступа к вектору предикатов
TClause* operator[](int x)const{return list[x];};
protected:
// функция поиска предиката по имени
int FindProc(const char*);
// функция возвращает число имеющихся предикатов
int GetProcCount(){return Count;}
// добавляет новый предикат в заданную позицию
// (последним в списек кандидатов на вызов)
int AddAt(TClause* p,int pos);
// добавляет новый предикат
int Add(TClause* p);
// очищает список предикатов
void Flush();
// завершает работу интерпретатора, устанавливает необходимые
// связи между переменными
void Link();
// текущя строка и
int Line;
// текущя позиция в тексте
int Pos;
// интерпретируемый текст
char* Text;
// текущий (создаваемый в процессе интерпретации) предикат
TClause* creature;
// текущий (создаваемый в процессе интерпретации) вызов
// предиката
TProcCall* call;
// алфавит интерпретатора
enum prefix {endtext=0, startList='(', endList=')', next=',
',end='.', anonimous='_', minus='-', number='N',
identifier='I', endDecl='E', undef=255};
// перечисление зарезервированных слов
enum reserved {userdef=0,integer};
// интерпретация текущего символа
prefix GetPrefix();
// поиск зарезервированного слова
reserved IsReservedWord(char* c);
// функция порождения исключительной ситуации
void Error(const char* msg);
// Чтение высказывания
void Expression();
61
// Чтение заголовка предиката
void Declaration();
// Чтение списка формальных параметров текущего предиката
void ParamList(TProcID* pID, TViewArea& ViewArea);
// Чтение списка подцелей текущего предиката
void CallList();
// возвращает число из текста определений
int Number();
// возвращает идентификатор из текста определений
void Identifier(char* name);
};
// класс следа.
class TTrace
{
protected:
// следующий след
TTrace* next;
// указатель на переменную, значение которой следует вернуть при
откате в
// состояние неопределенности
TFrameVar* var;
public:
TTrace(TFrameVar* _var):next(0),var(_var){}
// деструктор - возвращает переменной неопределенное состояние
// и удаляет оставшийся список следов. Таким образом, удалив один
след,
// мы удаляем весь стек следов
~TTrace()
{
var->State=undefined;
if(next) delete next;
}
// добавление следа в стек
TTrace* Push(TTrace* t){t->next=this;return t;}
};
// класс фрейма
class TFrame
{
protected:
// состояние переменных области видимости
TFrameVar* varState;
// стек следа
TTrace* Trace;
public:
// признак необходимости инициализации СК
bool isNew;
// родительское состояние
TFrame* ParentState;
62
// предыдущее состояние
TFrame* PrevState;
// точка выхода
TProcCall* ExitPoint;
// следующий вызов
TProcCall* CallList;
// следующий кандидат
TClause* NextProc;
TFrame(TFrame* parentState, TProcCall* callList, TProcCall*
exitPoint, int varCount): isNew(true),
ParentState(parentState), PrevState(0), varState(0),
CallList(callList), ExitPoint(exitPoint), Trace(0),
NextProc(0)
{
if(varCount)
varState=new TFrameVar[varCount];
};
// деструктор удаляет след данного фрейма и затем вектор
// переменных.
// таким образом, все присваивания из данного фрейма отменяются
~TFrame()
{
if (Trace) delete Trace;
if(varState)delete[] varState;
};
bool GetVarValue(int iVar, long& Value);
bool UnifyVar(int iVar,TFrame* pOwner, long Value);
bool UnifyVar(int iVar, int iDestVar, TFrame* pFrame);
// добавление фрейма в стек
void Push(TFrame* prev)
{
PrevState=prev;
}
};
// класс интерпретатора определений
class TInferenceMashine
{
public:
TInferenceMashine(int searchDepth,char* programText);
~TInferenceMashine();
// функция начала поиска решений. Возвращает указатель на
//достигнутое целевое состояние или NULL в случае неудачи
TFrame* FindSolution();
// возвращает вершину стека (где находится фрейм целевого
// предиката)
TFrame* GetTop(){return Top;}
TClause* GetGoalPredicate(){return ProcList[0];}
protected:
//-----------------------------------------------// функция вызова предиката ccp из фрейма State
63
TFrame* UnifyAndCall(TFrame* State,TProcCall* ccp);
//-----------------------------------------------// функция отката при неудаче
void BackTracking();
//-----------------------------------------------protected:
// глубина стека
int Depth;
// текущая глубина стека
int CurrDepth;
// признак прерывания поиска
int Terminated;
// признак первого поиска
int FirstSearch;
// внутреннее представление программы
TProcList ProcList;
// текущий фрейм
TFrame* Current;
// служебные указатели
TFrame* Point;
TFrame* Top;
};
#endif
Файл шаблонов “commtpl.h”
#if !defined(__hypgraph_h)
#define __hypgraph_h
#pragma hdrstop
#include <iostream.h>
template<class T> class TArrayTemplate
{
protected:
int size;
int delta;
T * array;
int count;
public:
TArrayTemplate(int _size=1,int
_delta=10):size(_size),delta(_delta),count(0)
{
array=new T[size];
}
TArrayTemplate(const TArrayTemplate<T>& a );
~TArrayTemplate()
{
delete[] array;
}
T operator[](int x) const {return array[x];}
T& operator[](int x) {return array[x];}
int operator==(const TArrayTemplate<T>& a);
int GetCount() const {return count;}
void Flush()
64
{
delete[] array;
array=new T[1];
size=1;
count=0;
}
void Add(const T & x)
{
if(size<=count && !Resize(size+delta))return;
array[count++]=x;
}
void SetAt(int idx,const T& x){
if(size<=idx && !Resize(idx+1))return;
array[idx]=x;
if(count<idx+1)count = idx+1;
}
void Delete(int idx);
void Clear(){count=0;}
TArrayTemplate<T>& operator=(const TArrayTemplate<T>& a );
protected:
int Resize(int s);
};
template <class T>
TArrayTemplate<T>::TArrayTemplate(const TArrayTemplate<T>& a )
{
array=new T[a.size];
size=a.size;
delta=a.delta;
count=a.count;
for(int i=0;i<a.count;i++)array[i]=a.array[i];
}
template <class T>
TArrayTemplate<T>& TArrayTemplate<T>::operator=(const
TArrayTemplate<T>& a )
{
Clear();
if(size<a.size)
{
delete[] array;
array=new T[a.size];
size=a.size;
}
count=a.count;
for(int i=0;i<a.count;i++)array[i]=a.array[i];
return *this;
}
template <class T>
int TArrayTemplate<T>::Resize(int s)
{
if(!delta)return 0;
T *tmp;
size=s;
tmp=new T[size];
65
for(int i=0;i<count;i++)tmp[i]=array[i];
delete[] array;
array=tmp;
return 1;
}
template <class T>
void TArrayTemplate<T>::Delete(int idx)
{
for(int i=idx;i<count-1;i++)array[i]=array[i+1];
count--;
}
template <class T>
int TArrayTemplate<T>::operator==(const TArrayTemplate<T>& a)
{
if(count!=a.count)return 0;
for(int i=0;i<count;i++)if(!(array[i]==a.array[i]))return 0;
return 1;
}
#endif
Файл “cmpspin.cpp”
#include
#include
#include
#include
<stdio.h>
<string.h>
<ctype.h>
<stdlib.h>
#include "engine.h"
#include "hypgraph.h"
//-----------------------------------------------------------// Список параметров
TViewArea::~TViewArea()
{
int iMax = Variables.GetCount();
for(int i=0;i<iMax;i++)
delete Variables[i];
iMax = Constants.GetCount();
for(i=0;i<iMax;i++)
delete Constants[i];
}
TParameter* TViewArea::GetVariable(LPCSTR paramName)
{
int iMax = Variables.GetCount();
for(int i=0;i<iMax;i++)
if(!strcmp(Variables[i]->Name, paramName))
return Variables[i];
TParameter* pVar=new TParameter(paramName,TParameter::var,
iMax);
Variables.Add(pVar);
66
return pVar;
}
TParameter* TViewArea::GetConstant(int constVal)
{
int iMax = Constants.GetCount();
for(int i=0;i<iMax;i++)
if(Constants[i]->Value == constVal)
return Constants[i];
TParameter* pConst = new TParameter("",TParameter::constant,
constVal);
Constants.Add(pConst);
return pConst;
}
TParameter* TViewArea::GetAnonimous()
{
int iMax = Constants.GetCount();
for(int i=0;i<iMax;i++)
if(!strcmp(Constants[i]->Name, "_"))
return Constants[i];
TParameter* p=new TParameter("_",TParameter::anonimous);
Constants.Add(p);
return p;
}
TProcID::TProcID(const char* name):ParamList(0,2)
{
strcpy(Name,name);
}
//-----------------------------------------------------------// Вызов предиката
int TProcCall::CanUnify(TClause* predicate,TFrame* State)
{
int ParamCount=ParamList.GetCount();
for(int i=0;i<ParamCount;i++)
{
// формальный параметр
TParameter* predParam=predicate->ParamList[i];
// фактический параметр
TParameter* callParam=ParamList[i];
switch(callParam->valType)
{
case TParameter::anonimous://анонимная переменая унифицируется с
чем угодно
break;
case TParameter::constant:
// константа не унифицируется с неравной ей константой
if(predParam->valType==TParameter::constant &&
predParam->Value!= callParam->Value ) return 0;
break;
case TParameter::var:
if(predParam->valType==TParameter::constant)
{
67
long lVal;
if(State->GetVarValue(callParam->Value, lVal)&& lVal
!= predParam->Value)
return false;
}
break;
}
}
return 1;
}
//----TFrame*
TProcCall::Execute(TClause* predicate, TFrame* State)
{
// предварительный отсев
if(! CanUnify(predicate,State) )
{
// и возрат признака неудачи
return 0;
}
// создаем новый фрейм
// State - родительский фрейм,
// predicate - предикат, породивший фрейм
// predicate->CallList - список подцелей, которые необходимо
доказать для
// проверки истинности predicate
// Next - точка выхода, следующий вызов для даного вызова
TFrame* newState=new TFrame(State,predicate->CallList,
Next,predicate->ViewArea.GetVarCount());
// производим унификацию
int ParamCount=ParamList.GetCount();
for(int i=0;i<ParamCount;i++)
{
switch(ParamList[i]->valType)
{
case TParameter::anonimous:// с анонимной переменной ничего не
делаем
break;
case TParameter::constant:
// если фактический параметр - константа
switch(predicate->ParamList[i]->valType)
{
case TParameter::anonimous:
// с анонимной переменной ничего не делаем
break;
// если формальный параметр - константа,
case TParameter::constant:
//этот случай мы проверили выше (функция CanUnify)
break;
// если формальный параметр - переменная
case TParameter::var:
{
if(!newState->UnifyVar(predicate->ParamList[i]->Value,
newState, ParamList[i]->Value))
{
68
delete newState;
return 0;
}
break;
}
}
break;
// если фактический параметр - переменная
case TParameter::var:
switch(predicate->ParamList[i]->valType)
{
case TParameter::anonimous:
// с анонимной переменной ничего не делаем
break;
case TParameter::constant:
// унификация переменной и константы. То же, что в
аналогичном случае
// выше, но присваивание происходит в противоположную
сторону
if(!newState->UnifyVar(ParamList[i]->Value,State
,predicate->ParamList[i]->Value) )
{
delete newState;
return 0;
}
break;
case TParameter::var:
if(!newState->UnifyVar(predicate->ParamList[i]->
Value, ParamList[i]->Value, State))
{
delete newState;
return 0;
}
break;
}
}
}
//возврат результатов
return newState;
}
//-----------------------------------------------------------//
// Список предикатов
//
//------------------------------------------------------------TProcList::TProcList():Count(0),Size(1),Delta(4),creature(0)
{
list=new TClause*[Size];
}
TProcList::~TProcList()
{
for (int i=0;i<Count;i++)delete list[i];
delete[] list;
}
69
int TProcList::FindProc(const char* pName)
{
for(int i=0;i<Count && strcmp(pName,list[i]->Name)!=0;i++);
return i;
}
int TProcList::Add(TClause* p)
{
int pos=FindProc(p->Name);
if(pos<Count)
{
TClause* lp=list[pos];
if(p->ParamList.GetCount()!=lp->ParamList.GetCount() )
Error("Параметры предиката не соответствуют ранее
декларированным");
while(lp->Next)lp=lp->Next;
lp->Next=p;
return pos;
}
if(Count==Size)
{
Size+=Delta;
TClause** tmp=new TClause*[Size];
for (int i=0;i<Count;i++)tmp[i]=list[i];
delete[] list;
list=tmp;
}
list[Count++]=p;
return Count-1;
}
int TProcList::AddAt(TClause* p,int pos)
{
if( pos>=Count|| strcmp(p->Name,list[pos]->Name)!=0 )return
Add(p);
TClause* lp=list[pos];
while(lp->Next)lp=lp->Next;
lp->Next=p;
return pos;
}
void TProcList::Flush()
{
for (int i=0;i<Count;i++)delete list[i];
Count=0;
Size=1;
delete[] list;
list=new TClause*[Size];
}
//-------------------------------------------int TProcList::Compile(char* text)
{
Line=1;
Pos=0;
70
Text=text;
creature=0;
call=0;
prefix p=GetPrefix();
while(p!=endtext)
{
if(p!=identifier) Error("Пропущено имя предиката");
Expression();
p=GetPrefix();
};
if(Count==0)Error("Нет текста программы");
creature=0;
call=0;
Link();
return 1;
}
void TProcList::Error(const char* msg)
{
if(creature)delete creature;
throw TSyntaxError(msg,Line,Pos);
}
TProcList::prefix TProcList::GetPrefix()
{
while(isspace(Text[Pos]))
{
if(Text[Pos]=='\n')Line++;
Pos++;
}
switch(Text[Pos])
{
case 0 : return endtext;
case '(': return startList;
case ')': return endList;
case ',': return next;
case '.': return end;
case ':': if(Text[++Pos]=='-')
{
Pos++;
return endDecl;
}
else Error("Пропущен ""-"" ");
case '_': if(isalpha(Text[Pos+1])||
(Text[Pos+1]>='0'&&Text[Pos+1]<='9'))return identifier;
else
{
Pos++;
return anonimous;
}
case '-': return minus;
case '0':case '1':case '2':case '3':case '4':
case '5':case '6':case '7':case '8':case '9':return number;
71
default:if( isalpha(Text[Pos]) )return identifier;
else Error("Неправильный символ");
return undef;
}
}
void TProcList::Link()
{
for(int i=0;i<Count;i++)
{
TClause* pred=list[i];
while (pred)
{
TProcCall* pc=pred->CallList;
char msg[255];
while (pc)
{
int max=pc->ParamList.GetCount();
if(pc->ProcId==undefined)
{
pc->ProcId=FindProc(pc->Name);
if(pc->ProcId>=Count)
{
sprintf(msg,"Неизвестный идентификатор %s в предикате/
%s ",pc->Name,pred->Name);
Error(msg);
}
TClause* calledPred=list[pc->ProcId];
if(calledPred->ParamList.GetCount()!=max)
{
sprintf(msg,"Неправильное число параметров при вызове/
предиката %s из предиката %s ",pc->Name,pred->Name);
Error(msg);
}
}
pc=pc->Next;
}
pred=pred->Next;
}
}
}
int TProcList::Number()
{
char ns[10];
int pos=0;
int sign=1;
if(GetPrefix()==minus)
{
Pos++;
sign=-1;
if(GetPrefix()!=number)Error("Константа пропущена");
}
while( Text[Pos] >='0'&& Text[Pos] <= '9' )
72
{
ns[pos++]=Text[Pos++];
if(pos>10) Error("Слишком длинная константа");
}
ns[pos]=0;
if(pos==0) Error("Константа пропущена");
return sign*atoi(ns);
}
void TProcList::Identifier(char* name)
{
int pos=0;
while(isalpha(Text[Pos] ) ||
( Text[Pos] >='0'&& Text[Pos] <= '9'))
{
name[pos++]=Text[Pos++];
if(pos>31) Error("Слишком длинный идентификатор");
}
if(!pos) Error("Пропущен идентификатор");
name[pos]=0;
}
void TProcList::Expression()
{
creature=0;
for(;;)
switch (GetPrefix())
{
case identifier:Declaration();
break;
case end:if(creature){
Add(creature);
creature=0;
Pos++;
return;
}
else Error("Пропущен идентификатор");
default: Error("Неправильный символ");
}
}
void TProcList::Declaration()
{
char procName[32];
Identifier(procName);
if(IsReservedWord(procName))Error("Неправильное использование
зарезервированного слова");
creature=new TClause(procName);
int pref=GetPrefix();
if(pref==startList) {
Pos++;
ParamList(creature,creature->ViewArea );
pref=GetPrefix();
73
}
switch(pref)
{
case endDecl:CallList();
break;
case end:return;
default:Error("Неправильный символ");
}
}
TProcList::reserved
TProcList::IsReservedWord(char* /*c*/)
{
return userdef;
}
void TProcList::ParamList(TProcID* pID, TViewArea& ViewArea)
{
char fName[32];
for(;;)
{
switch(GetPrefix())
{
case identifier:
Identifier(fName);
pID->ParamList.Add(ViewArea.GetVariable(fName) );
break;
case minus:
case number:{
int val=Number();
pID->ParamList.Add(ViewArea.GetConstant(val) );
break;
}
case anonimous:
pID->ParamList.Add(ViewArea.GetAnonimous());
break;
}
switch(GetPrefix())
{
case next:Pos++;
break;
case endList:
Pos++;
return;
default:Error("Неправильный символ");
}
}
}
void TProcList::CallList()
{
char name[32];
call=0;
74
int cpRead=0;
for(int p=GetPrefix();;p=GetPrefix())
{
switch(p)
{
case identifier:
if(call) Error("Пропущена "",""");
Identifier(name);
switch(IsReservedWord(name))
{
case userdef:
call=new TProcCall(name);
creature->Add(call);
break;
default: Error("Неправильное использование зарезервированного
слова");
}
break;
case anonimous:
case minus:
case number:
{
Error("Бессмысленная операция");
}
break;
case next:Pos++;
if(call)CallList(); else Error("Пропущен идентификатор");
break;
case startList:Pos++;
if(call && !cpRead){
ParamList(call, creature->ViewArea);
cpRead=1;
}else Error("Пропущен идентификатор");
break;
case end:if(call)return;
else Error("Пропущен идентификатор");
case endtext:
Error("\".\" Пропущена");
}
}
}
//-------------------------------------------// Фрейм
bool TFrame::GetVarValue(int iVar, long& Value)
{
TFrameVar* srcVS = &varState[iVar];
// итерация по указателям до значения переменной
while(srcVS->State==ptr)srcVS=srcVS->Value.Pointer;
if(srcVS->State == undefined)return false;
else Value=srcVS->Value.Integer;
return true;
75
}
bool TFrame::UnifyVar(int iVar,TFrame* pOwner, long Value)
{
TFrameVar* srcVS = &pOwner->varState[iVar];
// итерация по указателям до значения переменной
while(srcVS->State==ptr)srcVS=srcVS->Value.Pointer;
switch(srcVS->State)
{
case undefined:
srcVS->Value.Integer=Value;
srcVS->State=bound;
Trace = Trace->Push(new TTrace( srcVS ));
return true;
case bound:
return srcVS->Value.Integer==Value;
}
return false;
}
bool TFrame::UnifyVar(int iVar, int iDestVar, TFrame* pFrame)
{
TFrameVar* srcVS = &varState[iVar];
while(srcVS->State==ptr)srcVS=srcVS->Value.Pointer;
TFrameVar* destVS = &pFrame->varState[iDestVar];
while(destVS->State==ptr)destVS=destVS->Value.Pointer;
switch(srcVS->State)
{
case bound:
switch(destVS->State)
{
case bound:
return srcVS->Value.Integer==destVS->Value.Integer;
case undefined:
destVS->Value.Integer=srcVS->Value.Integer;
destVS->State=bound;
Trace = Trace->Push(new TTrace( destVS ));
return true;
}
break;
case undefined:
switch(destVS->State)
{
case bound:
srcVS->Value.Integer=destVS->Value.Integer;
srcVS->State=bound;
Trace = Trace->Push(new TTrace( srcVS ));
return true;
case undefined:
if(destVS!=srcVS)
{
destVS->State=ptr;
destVS->Value.Pointer=srcVS;
Trace = Trace->Push(new TTrace( destVS ));
}
76
return true;
}
break;
}
return false;
}
//-------------------------------------------// Интерпретатор определений
TInferenceMashine::TInferenceMashine(int searchDepth,char*
programText):
Depth(searchDepth),FirstSearch(1),
Terminated(0),CurrDepth(0)
{
// компилируем текст определений
ProcList.Compile(programText);
// создаем фрейм целевого утверждения (индекс 0 в списке
предикатов)
Top=Point=Current=new TFrame(0,ProcList[0]>CallList,0,ProcList[0]->ViewArea.GetVarCount() );
};
TInferenceMashine::~TInferenceMashine()
{
// удаление стека, если таковой остался
while(Current)
{
TFrame* Element=Current;
Current=Current->PrevState;
delete Element;
}
};
void TInferenceMashine::BackTracking()
{
// возврат при неудаче
for(;;)
{
// если текущий фрейм - точка возврата
if(Current->NextProc)return; //- выход
// если откат произошел до целевого утверждения, а,
следовательно
// больше решений нет
if(!Current->PrevState)
{
// прерывание интерпретации
Terminated=1;
return;
}
TFrame* Element=Current;
Current=Current->PrevState;
// удаляем текущий фрейм (происходит в том числе и отмена всех
присваиваний,
77
// связанных с ним)
delete Element;
CurrDepth--;
}
};
TFrame*
TInferenceMashine::UnifyAndCall(TFrame* State,TProcCall* ccp)
{
if(Current->isNew)
{
Current->NextProc=ProcList[ccp->ProcId];
Current->isNew=false;
}
// пока есть кандидаты на вызов
while(Current->NextProc)
{
// пытаемся унифицировать фактические и формальные параметры
//предиката
TFrame* res=ccp->Execute(Current->NextProc, State);
if(Current->NextProc)
Current->NextProc=Current->NextProc->Next;
// в случае удачи возвращаем новый фрейм
if(res) return res;
}
// если кандидатов не осталось
return 0;
}
TFrame* TInferenceMashine::FindSolution()
{
// инициализация
Terminated=0;
// если какое-то решение было найдено ранее, следует откатиться
назад
if( !FirstSearch )BackTracking(); else FirstSearch = 0;
while( !Terminated )
{
// отслеживаем ограничение глубины стека
if(CurrDepth > Depth ){break;};
TFrame* NewState;
// если в текущем фрейме есть подцель,
if(Current->CallList)
// приступаем к ее доказательству
NewState=UnifyAndCall(Current,Current->CallList);
// иначе
else
{
// все подцели доказываемого предиката доказаны
// следовательно, осуществляем успешный выход из предиката
TFrame* State=Current;
78
for(;;)
{
// если мы нашли точку выхода
// (нашли еще недоказанный вызов в предикате, в который
мы вышли)
if(State->ExitPoint)
{
// доказываем ее
NewState=UnifyAndCall(State->ParentState,State->ExitPoint);
break;
}else
// если мы достигли вершины стека
if(!State->ParentState)
{
// целевое утверждение доказано
Point=Current;
// возврат значения
return Current;
}
// иначе - спускаемся по стеку
else State=State->ParentState;
}
}
// если не удалось породить новый фрейм - откатываемся назад
if(!NewState ){ BackTracking();continue;};
// добавляем новый фрейм в стек
NewState->Push(Current);
// и делаем его текущим
Current=NewState;
CurrDepth++;
}
// поиск завершен неудачно
FirstSearch=1;
return 0;
};
79
Оглавление
1. Введение .................................................................................................... 3
2. Основы логического программирования ............................................... 4
Язык логики предикатов .......................................................................... 4
Метод резолюций ..................................................................................... 5
3. Реализация идей логического программирования в языке Пролог ... 11
Структура программы на языке Турбо Пролог ................................... 11
Простейшие программы ........................................................................ 12
Рекурсия .................................................................................................. 15
Управление доказательством ................................................................ 20
Примеры аксиоматизации и решения задач ......................................... 23
4. Качественное моделирование непрерывных систем ........................... 25
Аксиомы качественного моделирования ............................................. 25
Реализация качественного моделирования .......................................... 34
5. Реализация Пролог–машины ................................................................. 34
Вычислительная модель ........................................................................ 34
Объектная реализация ............................................................................ 39
Список литературы .................................................................................... 44
Приложения ................................................................................................ 45
Приложение 1 .............................................. Error! Bookmark not defined.
Реализация определений качественного моделирования на языке
Пролог ......................................................................................................... 45
Приложение 2 .............................................. Error! Bookmark not defined.
Объектная реализация Пролог-машины ................................................... 50
Классы объектной модели ..................................................................... 50
Исходный текст ...................................................................................... 57
Оглавление .................................................................................................. 80
80
Related documents
Download