МУРОМСКИЙ ИНСТИТУТ (ФИЛИАЛ) ВЛАДИМИРСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ Представление знаний в информационных системах. Логическое программирование Методические указания к лабораторным работам Муром 2005 УДК 62-506 С Рецензенты: к.т.н., доцент Жизняков А.Л., к.т.н., доцент Терсин В.В. Середа С.Н. Представление знаний в информационных системах. Логическое программирование/ Методические указания к лабораторным работам. – Муром, МИ ВлГУ, 2005. – 80 с. , ил., табл. ISBN Методические пособия к лабораторным работам дополняют цикл лекций по предмету «Представление знаний в информационных системах», являющемуся федеральным компонентом государственного образовательного стандарта для специальности 071900 – «Информационные системы (по областям применения)». Одним из ключевых вопросов дисциплины является освоение студентами ВУЗов, обучающимся по указанной специальности, навыков логического программирования на языке ПРОЛОГ и разработке экспертных систем. В учебном курсе используются ПРОЛОГ - языки и среды программирования как российских, так и западных разработчиков исключительно в не коммерческих целях. УДК 62 -506 ISBN © Муромский институт (филиал) Владимирского государственного университета, 2005 © Середа С.Н., 2005 2 Лабораторная работа №1 Тема: Основы логического программирования на языке ПРОЛОГ. Цель: Изучение принципов логического программирования на языке ПРОЛОГ, математического описания логических задач. Задание В соответствии с вариантом задания разработать: 1) математическую модель решения логической задачи. 2) написать программу поиска решения на языке ПРОЛОГ. В отчет включить: 1) постановку задачи 2) описание математической модели 3) листинг программы Теоретическая часть В Прологе (Prolog — PROgrammmg LOGic) решение задачи получается логическим выводом из ранее известных положений. Обычно программа на Прологе не является последовательностью действий, — она представляет собой набор фактов с правилами, обеспечивающими получение заключений на основе этих фактов. Поэтому Пролог известен как декларативный язык. Пролог базируется на предложениях Хорна, являющихся подмножеством формальной системы, называемой логикой предикатов. Логика предикатов — это простейший способ объяснить, как “работает” мышление, и она проще, чем арифметика, которой вы давно пользуетесь. Пролог использует упрощенную версию синтаксиса логики предикатов, он прост для понимания и очень близок к естественному языку. Пролог включает механизм вывода, который основан на сопоставлении образцов. С помощью подбора ответов на запросы он извлекает хранящуюся (известную) информацию. Пролог пытается проверить истинность гипотезы (другими словами — ответить на вопрос), запрашивая для этого информацию, о которой уже известно, что она истинна. Прологовское знание о мире — это ограниченный набор фактов (и правил), заданных в программе. Одной из важнейших особенностей Пролога является то, что в дополнение к логическому поиску ответов на поставленные вопросы он может иметь дело с альтернативами и находить все возможные решения. Вместо обычной работы от начала программы до ее конца, Пролог может возвращаться назад и просматривать более одного “пути” при решении всех составляющих задачу частей. Логика предикатов была разработана для наиболее простого преобразования принципов логического мышления в записываемую форму. Пролог использует преимущества синтаксиса логики для разработки программного языка. Здесь исключаются из высказываний все несущественные слова. Затем предложения преобразуются по форме, где на первое место ставятся отношения, а после него — сгруппированные объекты. В дальнейшем объекты становятся аргументами, между которыми устанавливается это 3 отношение. В качестве примера в таблице 1.1 представлены предложения, преобразованные в соответствии с синтаксисом логики предикатов. Таблица 1.1 Синтаксис логики предикатов Предложения на естественном языке Синтаксис логики предикатов Машина красивая fun (car) Роза красная red (rose) Билл любит машину, если машина likes(bill, Car) if fun (Car) красивая Примечание: В высказываниях на Прологе все известные факты и данных записываются со строчной, а переменные – с прописной букв. Факты и правила Программист на Прологе описывает объекты и отношения, а затем описывает правила, при которых эти отношения являются истинными. Например, предложение Билл любит собак. (Bill likes dogs.) устанавливает отношение между объектами Bill и dogs (Билл и собаки), этим отношением является likes (любит). Ниже представлено правило, определяющее, когда предложение "Билл любит собак" является истинным: Билл любит собак, если собаки хорошие. (Bill likes dogs if the dogs are nice.) Факты В Прологе отношение между объектами называется фактом. В естественном языке отношение устанавливается в предложении. В логике предикатов, используемой Прологом, отношение соответствует простой фразе (факту), состоящей из имени отношения и объекта или объектов, заключенных в круглые скобки. Как и предложение, факт завершается точкой (.). Ниже представлено несколько предложений на естественном языке с отношением "любит" (likes) и соответствие высказываний на Прологе: Билл любит Синди. (Bill likes Cindy) likes(bill, cindy). Синди любит Билла. (Cindy likes Bill) likes(cindy, bill). Билл любит собак. (Bill likes dogs) likes(bill, dogs). Факты, помимо отношений, могут выражать и свойства. Так, например, предложения естественного языка 'Kermit is green" {Кермит зеленый) и 'Caitlin is girl" (Кейтлин — девочка) на Прологе, выражая те же свойства, выглядят следующим образом: green (kerrrat). girl(caitlin). Правила Правила позволяют вам вывести один факт из других фактов. Другими словами, можно сказать, что правило — это заключение, для которого известно, что оно истинно, если одно или несколько других найденных заключений или фактов являются истинными. Ниже представлены правила, соответствующие связи "любить" (likes): 4 Синди любит все, что любит Билл. (Cindy likes everything that Bill likes) Кейтлин любит все зеленое. (Caitlin likes everything that is green) Используя эти правила, вы можете из предыдущих фактов найти некоторые вещи, которые любят Синди и Кейтлин: Синди любит Синди. (Cindy likes Cindy) Кейтлин любит Кермит. (Caitlin likes Kermit) Чтобы перевести эти правила на Пролог, вам нужно немного изменить синтаксис, подобно этому: likes (cindy, Something) :- likes (bill, Something). likes (caitlin, Something) :- green (Something). Символ :- имеет смысл "если"; и служит для разделения двух частей правила: заголовка и тела. Правило можно рассматривать и как процедуру. Другими словами, эти правила likes (cindy, Something) :- likes (bill, Something) likes (caitlin, Something) :- green (Something). также означают, "Чтобы доказать, что Синди любит что-то, докажите, что Билл любит это" и "Чтобы доказать, что Кейтлин любит что-то, докажите, что это что-то зеленое". С такой "процедурной" точки зрения правила могут "попросить" Пролог выполнить другие действия, отличные от доказательств фактов, — такие как напечатать что-нибудь или создать файл. Объекты Программа на языке Пролог обычно описывает некую действительность. Объекты (элементы) описываемого мира представляются с помощью термов. Терм интуитивно означает объект. Существует 4 вида термов: атомы, числа, переменные и составные термы. Атомы и числа иногда группируют вместе и называют простейшими термами. Атом - это отдельный объект, считающийся элементарным. В Прологе атом представляется последовательностью букв нижнего и верхнего регистра, цифр и символа подчеркивания '_', начинающейся со строчной буквы. Кроме того, любой набор допустимых символов, заключенный в апострофы, также является атомом. Наконец, комбинации специальных символов + - * = < > : & также являются атомами (следует отметить, что набор этих символов может отличаться в различных версиях Пролога). Пример. Представленные далее последовательности являются корректными атомами: b, abcXYZ, x_123, efg_hij, коля, слесарь, 'Это также атом Пролога', +, ::, <---->, *** Числа в Прологе бывают целыми (Integer) и вещественными (Float). Синтаксис целых чисел прост, как это видно из следующих примеров: 1, 1313, 0, -97. Не все целые числа могут быть представлены в машине, их диапазон ограничен интервалом между некоторыми минимальным и максимальным значениями, определенными конкретной реализацией Пролога. Синтаксис 5 вещественных чисел также зависит от конкретной реализации. При обычном программировании на Прологе вещественные числа используются редко, поскольку Пролог - язык, предназначенный в первую очередь для обработки символьной, а не числовой информации. При символьной обработке часто используются целые числа, нужда же в вещественных числах невелика. Везде, где можно, Пролог старается привести число к целому виду. Переменными в Прологе являются строки символов, цифр и символа подчеркивания, начинающиеся с заглавной буквы или символа подчеркивания: X, _4711, X_1_2, Результат, _x23, Объект2, _ Последний пример (единственный символ подчеркивания) является особым случаем - анонимной переменной (переменной без имени). Анонимная переменная применяется, когда ее значение не используется в программе. Возможное неоднократное употребление безымянной переменной в одном выражении применяется для того, чтобы подчеркнуть наличие переменных при отсутствии их специфической значимости. Составные термы (функции) состоят из имени функции (нечислового атома) и списка аргументов (термов Пролога, то есть атомов, чисел, переменных или других составных термов), заключенных в круглые скобки и разделенных запятыми. Группы составных термов используют для составления фраз Пролога. Нельзя помещать символ пробела между функтором (именем функции) и открывающей круглой скобкой. В других позициях, однако, пробелы могут быть полезны для создания более читаемых программ. Ниже приведены два составных терма: итого(клиент(X,23,_), 71) 'Что случилось?'(ничего) При задании имен термов предпочтительнее использовать мнемонические ("говорящие") имена, так как терм a(ж), например, гораздо менее информативен, чем терм aвтор(жюль_верн). Еще одной важной структурой данных в Прологе является список. Сейчас отметим только один из видов списков - список символов. Такие списки могут быть представлены в виде строк, например, первый аргумент составного терма возраст("Борис",10) - строка. При записи строки заключаются в кавычки. Запросы Однократно предоставив языку Пролог несколько фактов, можно задавать вопросы, касающиеся отношений между ними. Это называется запросом системы языка Пролог. Основываясь на известных, заданных ранее фактах и правилах, можно ответить на вопросы об этих отношениях, в точности так же это может сделать Пролог. На естественном языке мы спрашиваем: Does Bill like Cindy? (Билл любит Синди?) По правилам Пролога мы спрашиваем: likes(bill, cindy). Получив такой запрос, Пролог мог бы ответить: yes (да). 6 потому что Пролог имеет факт, подтверждающий, что это так. Немного усложнив вопрос, мы могли бы спросить вас на естественном языке: What does Bill like? (Что любит Билл?) По правилам Пролога мы спрашиваем: likes (bill, What). Заметим, что синтаксис Пролога не изменяется, когда вы задаете вопрос: этот запрос очень похож на факт. Впрочем, важно отметить, что второй объект — What — начинается с большой буквы, тогда как первый объект — bill — нет. Это происходит потому, что bill — фиксированный, постоянный объект — известная величина, a What — переменная. Пролог всегда ищет ответ на запрос, начиная с первого факта, и перебирает все факты, пока они не закончатся. Получив запрос о том, что Билл любит, Пролог ответит: What=cindy What=dogs 2 Solutions Так как ему известно, что likes(bill, cindy). и likes(bill, dogs). Надеемся, что вы пришли к такому же выводу. Если бы мы спросили вас (и Пролог): What does Cindy like? (Что любит Синди?) likes(cindy, What). то Пролог ответил бы: What = bill What = cindy What = dogs 3 solutions поскольку Пролог знает, что Синди любит Била, и что Синди любит то же, что и Билл, и что Билл любит Синди и собак. Мы могли бы задать Прологу и другие вопросы, которые можно задать человеку. Но вопросы типа "Какую девушку любит Билл?" не получат решения, т.к. Прологу в данном случае не известны факты о девушке, а он не может вывести заключение, основанное на неизвестных данных: в этом примере мы не дали Прологу какого-нибудь отношения или свойства, чтобы определить, являются ли какие-либо объекты девушками. Размещение фактов, правил и запросов Предположим, у вас есть следующие факты и правила: Быстрая машина — приятная. (A fast car is fun). Большая машина — красивая. (A big car is nice). Маленькая машина — практичная. (A little car is practical). Биллу нравится машина, если она приятная. (Bill likes a car if the car is fun). 7 Исследуя эти факты, можно сделать вывод, что Билу нравится быстрый автомобиль. В большинстве случаев Пролог придет к подобному решению. Если бы не было фактов о быстрых автомобилях, вы не смогли бы логически вывести, какие автомобили нравятся Биллу. Вы можете делать предположения о том, какой тип машин может быть крепким, но Пролог знает только то, что вы ему скажете, Пролог не строит предположений. Вот пример, демонстрирующий, как Пролог использует правила для ответа на запросы. Посмотрите на факты и правила в части следующей программы: likes (ellen, tennis). likes (john, football). likes (tom, baseball). likes (eric, swimming). likes (mark, tennis). likes (bill, Activity) :- likes (tom, Activity). Это правило соответствует предложению естественного языка: Биллу нравится занятие, если Тому нравится это занятие. (Bill likes an activity if Tom likes that activity) В данном правиле заголовок — это likes (Bill, activity), а тело — likes (torn, Activity). Заметим, что в этом примере нет фактов о том, что Билл любит бейсбол. Чтобы выяснить, любит ли Билл бейсбол, можно дать Пролог такой запрос: goal likes (bill, baseball). Пролог ответит на этот запрос: yes (да) При этом Пролог использовал комбинированное правило likes(bill,Activity) :- likes(tom,Activity). с фактом likes(tom,baseball). для решения, что likes(bill, baseball). Краткие выводы по вышесказанному: 1. Программы на языке Пролог состоят из двух типов фраз: фактов и правил, также называемых предложениями. Факты — это отношения или свойства, о которых известно, что они имеют значение "истина". Правила — это связанные отношения; они позволяют Прологу логически выводить одну порцию информации из другой. Правило принимает значение "истина", если доказано, что заданный набор условий является истинным. 2. В Прологе все правила имеют 2 части: заголовок и тело, разделенные специальным знаком :-. Заголовок — это факт, который был бы истинным, если бы были истинными несколько условий. Это называется выводом или зависимым отношением. 8 Тело — это ряд условий, которые должны быть истинными, чтобы Пролог мог доказать, что заголовок правила истинен. Факты и правила — практически одно и то же, кроме того, что факты не имеют явного тела. Факты ведут себя так, как если бы они имели тело, которое всегда истинно. Пролог всегда ищет решение, начиная с первого факта и/или правила, и просматривает весь список фактов и/или правил до конца. Механизм логического вывода Пролога берет условия из правила (тело правила) и просматривает список известных фактов и правил, пытаясь удовлетворить условиям. Если все условия истинны, то зависимое отношение (заголовок правила) считается истинным. Если все условия не могут быть согласованы с известными фактами, то правило ничего не выводит. Предложения По сути, есть только два типа фраз, составляющих язык Пролога: фраза может быть либо фактом, либо правилом. Эти фразы в Прологе известны под термином предложения (clause). Сердце программ на Прологе состоит из предложений. Подробнее о фактах. Факт представляет либо свойство объекта, либо отношение между объектами. Факт самодостаточен. Прологу не требуется дополнительных сведений для подтверждения факта, и факт может быть использован как основа для логического вывода. Подробнее о правилах. В Прологе, как и в обычной жизни, можно судить о достоверности чего-либо, логически выведя это из других фактов. Правило — это конструкция Пролога, которая описывает, что можно логически вывести из других данных. Правило — это свойство или отношение, которое достоверно, когда известно, что ряд других отношений достоверен. Синтаксически эти отношения разделены запятыми. Рассмотрим несколько примеров работы с правилами. 1. Пример, иллюстрирующий правило, которое может быть использовано для того, чтобы сделать вывод, подходит ли некоторое блюдо из меню Диане: Диана — вегетарианка и ест только то, что говорит ей ее доктор. (Diane is a vegetarian and eats only what her doctor tells her to eat) Зная меню и предыдущее правило, можно сделать вывод о том, выберет ли Диана данное блюдо. Чтобы выполнить это, нужно проверить, соответствует ли блюдо заданному ограничению: • является ли Food_on_menu овощем? • находится ли Food_on_menu в списке доктора? • заключение: если оба ответа положительны, Диана может заказать Food_on_menu. В Прологе подобное отношение должно быть выражено правилом, т.к. вывод основан на фактах. Вот один вариант записи правила: diane_can_eat(Food_on_menu):vegetable(Food_on_menu), 9 on_doctor_list(Food_on_menu). Обратите внимание, что после vegetable (Food__on_menu) стоит запятая. Она обозначает конъюнкцию нескольких целей и читается как "и", оба правила — vegetable (Food_on_menu) и on_doctor_list (Food_on_meru) — должны быть истинны для истинности diane_can_eat(Food_on_menu). 2. Предположим, что вам хотелось бы записать факт, который истинен, если некто Рersonl является родителем кого-то Рerson2. Нужный факт выглядит так: parent(paul, samantha). Этот факт обозначает, что Пол — родитель Саманты. Но, предположим, что в базе данных фактов Пролога есть факты, формулирующие отношение отцовства. Например, "Пол — отец Саманты": father(paul, samantha). Пусть у вас также есть факты, формулирующие отношение материнства, например, "Джулия — мать Саманты": mother(julie, samantha). Если у вас уже есть несколько фактов, формулирующих отношения отцовства/материнства, то не стоит тратить время на запись факта родства в базу данных фактов для каждого отношения родства. Так как вы знаете, что Personl — родитель Рerson2, если Personl — отец Рerson2 или Personl — мать Person2, то почему бы не записать правило, объединяющее эти ограничения? После формулирования этих условий на естественном языке достаточно просто записать их в правило Пролога. parent(Personl, Person2):- father(personl, Person2). parent{Personl, Person2):- mother(Personl, Person2). Эти правила Пролога говорят, что Personl является родителем Рerson2, если Personl является отцом Person2. Personl является родителем Person2, если Personl является матерью Person2. Предикаты Отношение в Прологе называется предикатом. Аргументы — это объекты, которые связываются этим отношением; в факте likes (bill, cindy) отношение likes — это предикат, а объекты bill и cindy — аргументы. Вот несколько примеров предикатов с различным числом аргументов: pred(integer, symbol) person(last, first, gender) run() birthday(firstName, lastName, date) В вышеприведенном примере показано, что предикаты могут вовсе не иметь аргументов, но использование таких предикатов ограничено. Чтобы выяснить имя Mr.Rosemont, можно применить запрос person (rosemont, Name, male). Но что делать с запросом без аргументов run? Выясним, есть ли в программе предложение run, и если run — это заголовок правила, то можно вычислить данное правило. В некоторых случаях это оказывается полезным — 10 например, если бы вы захотели создать программу, работающую по-разному в зависимости от того, имеется ли предложение run. Переменные В простом запросе, чтобы найти того, кто любит теннис, можно использовать переменные. Например: likes(X, tennis). В этом запросе буква X используется как переменная для нахождения неизвестного человека. Осмысленный выбор имен переменных делает программу более удобной для чтения. Например: likes(Person, tennis). лучше, чем предыдущий вариант, потому что Person имеет больше смысла, чем переменная X. Инициализация переменных Пролог не имеет оператора присваивания. Это важное отличие Пролога от других языков программирования. Переменные в Прологе инициализируются при сопоставлении с константами в фактах или правилах. До инициализации переменная свободна; после присвоения ей значения она становится связанной. Переменная остается связанной только то время, которое необходимо для получения решения по запросу, затем Пролог освобождает ее и ищет другое решение. Нельзя сохранить информацию, присвоив значение переменной. Переменные используются как часть процесса поиска решения, а не как хранилище информации. Рассмотрим следующую программу: predicates likes(symbol,symbol) clauses likes(ellen,reading). likes(john,computers). likes(john,badminton). likes{leonard,badminton). likes(eric,swimming). likes(eric,reading). Рассмотрим запрос: есть ли человек, который любит и чтение, и плавание? likes(Person, reading), likes(Person, swimming). Пролог будет решать обе части запроса посредством поиска фактов с начала и до конца программы. В первой части запроса likes (Person, reading) переменная Person свободна; ее значение неизвестно перед тем, как Пролог попытается найти решение. С другой стороны, второй аргумент, reading, известен. Пролог ищет факт, который соответствует первой части запроса. Первый факт в программе likes (ellen, rerading) удовлетворяет первой части запроса (reading в факте соответствует reading в запросе), то есть Пролог связывает свободную переменную Person со значением ellen, соответствующим значению в факте. В то же время Пролог помещает указатель в список фактов, показывающий, как далеко продвинулась процедура 11 поиска. Далее, для полного разрешения запроса (поиск человека, которому нравится и чтение, и плавание) должна быть решена вторая часть запроса. Так как Person сейчас связана со значением ellen, Пролог должен искать факт likes(ellen, swimming). Пролог ищет этот факт от начала программы, но совпадения нет (потому что в программе нет такого факта). Вторая часть запроса ложна, если Person имеет значение ellen. Теперь Пролог освободит переменную Person и попытается найти иное решение первой части запроса. Поиск другого факта, удовлетворяющего первой части запроса, начинается с указателя в списке фактов (такое возвращение к отмеченной позиции называется поиском с возвратом). Пролог ищет следующего человека, кто любит чтение и находит факт likes (eric, reading). Переменная Person сейчас связана со значением eric, и Пролог пытается вновь найти соответствие со второй частью запроса посредством поиска в программе факта: likes(eric, swimming) Пролог находит совпадение (последнее предложение в программе), и запрос полностью удовлетворяется. Пролог возвращает Person=eric. 1 Solution. Анонимные переменные Анонимные переменные позволяют "привести в порядок" программы. Если вам нужна только определенная информация запроса, можно использовать анонимные переменные для игнорирования ненужных значений. В Прологе анонимные переменные обозначаются символом подчеркивания (_). Следующий пример демонстрирует использование анонимных переменных. predicates male(symbol) female(symbol) parent(symbol, symbol) clauses male(bill). male (jое). female(sue). female(tammy). parent(bill, joe). parent(sue, joe). parent(joe, tammy). Анонимная переменная может быть использована на месте любой другой переменной и ей никогда не присваивается значение. Например, в следующем запросе нам понадобится узнать, какие люди являются родителями, но вам неинтересно, кто их дети. Пролог знает, что каждый раз, когда вы используете символ подчеркивания в запросе, вам не нужна информация о значении, представленном на месте переменной. goal parent(Parent, _). Получив такой запрос, Пролог отвечает: Parent=bill 12 Parent=sue Parent=joe 3 Solutions В этом случае Пролог находит и выдает трех родителей, но он не выдает значения, связанные со вторым аргументом в предложении parent. Анонимные переменные также можно использовать в фактах. Следующие факты Пролога: owns(_, shoes). eats(_). могли быть использованы для выражения утверждений на естественном языке: У каждого есть туфли. (Everyone owns shoes) Каждый ест. (Everyone eats) Анонимные переменные сопоставляются с любыми данными. Цели (запросы) Формируя запрос, мы тем самым ставим цель Прологу, которую он должен достичь. Для объявления раздела цели в программе на Прологе используется служебное слово goal. Трактовка запросов как целей такова: когда вы даете Прологу запрос, в действительности вы даете ему цель для выполнения. Цели могут быть или простыми: likes(ellen, swimming). likes(bill, What), или более сложными. likes(Person, reading), likes(Person, swimming). Цель, состоящая из двух и более частей, называется сложной целью, а каждая часть сложной цепи называется подцелью. Часто бывает нужно найти общее решение двух целей. Например, в предыдущем примере о родителях вы могли бы поинтересоваться, которые из родителей являются родителями мужского пола. Искать решение для такого запроса можно, задав сложную цель. Задайте следующую сложную цель: goal parent(Person, _) , male(Person). Сначала Пролог попытается решить подцель parent(Person, _) путем поиска соответствующего предложения и последующего связывания переменной Person со значением, возвращенным parent (Person — это родитель). Значение, возвращаемое parent, далее предоставляется второй подцели в качестве значения, с которым будет продолжен поиск: является ли Person — теперь это связанная переменная — мужского пола? male(Person) Если цель задана корректно, Пролог ответит: Person=bill Person=joe 2 Solutions 13 Комментарии Хорошим стилем программирования является включение в программу комментариев, объясняющих все то, что может быть непонятно кому-то другому. Рекомендуется подбирать подходящие имена для переменных, предикатов и доменов, раскрывающих их смысл. Многострочные комментарии должны начинаться с символов /* (косая черта, звездочка) и завершаться символами */ (звездочка, косая черта). Для установки однострочных комментариев можно использовать либо эти же символы, либо начинать комментарий символом процента (%). /* Это первый пример комментария */ % Это второй пример комментария /************************************* А эти три строчки — пример многострочного комментария *************************************/ Сопоставление (унификация) Ранее было показано, как Пролог "сопоставляет вопросы и ответы", "ищет сопоставление", "сопоставляет условия с фактами", "сопоставляет переменные с константами" и т.д. Ниже рассмотрим, что же понимается под термином сопоставление. В Прологе имеется несколько примеров сопоставления одной вещи с другой. Ясно, что идентичные структуры сопоставимы (сравнимы) друг с другом: parent (joe, tammy) сопоставимо с parent (joe, tammy). Однако сопоставление (сравнение) обычно использует одну или несколько свободных переменных. Например, если X свободна, то parent (joe, X) сопоставимо с parent (joe, tammy) и X принимает значение (связывается с) tammy. Если же X уже связана, то она действует так же, как обычная константа. Таким образом, если X связана со значением tammy, то parent (joe, X) сопоставимо с parent (joe, tammy), но parent (joe, X) не сопоставимо с parent (joe, millie). В предыдущем примере сопоставление не выполняется, т.к. если переменная становится связанной, то ее значение не может изменяться. Как может переменная оказаться связанной при попытке Пролога сопоставить ее с чем-либо? Вспомним, что переменные не могут хранить значения, т.к. они становятся связанными только на промежуток времени, необходимый для отыскания (или попытки отыскания) одного решения цели. Поэтому имеется только одна возможность для переменной оказаться связанной — перед попыткой сопоставления, если цель требует больше одного шага, и переменная стала связанной на предыдущем шаге. Например: parent(joe,X), parent(X,jenny) является корректной целью. Она означает: "Найти кого-либо, являющегося ребенком Joe и родителем Jenny". Здесь при достижении подцели parent (X, jenny) переменная X уже будет связана. Если для подцели parent (X, jenny) нет решений, Пролог "развяжет" переменную X и вернется назад, пытаясь найти 14 новое решение для parent (joe, X), а затем проверит, будет ли "работать" parent (X, jenny) с новым значением X. Две свободные переменные могут сопоставляться друг с другом. Например, parent (joe, X) сопоставляется с parent (joe, Y), связывая при этом переменные X и Y между собой. С момента "связывания" X и У трактуются как одна переменная, и любое изменение значения одной из них приводит к немедленному соответствующему изменению другой. В случае подобного "связывания" между собой нескольких свободных переменных все они называются совмещенными свободными переменными. Некоторые методы программирования специально используют "взаимосвязывание" свободных переменных, являющихся, на самом деле, различными. В Прологе связывание переменных (со значениями) производится двумя способами: на входе и выходе. Направление, в котором передаются значения, указывается в шаблоне потока параметров (flow pattern). В дальнейшем (для краткости) будем опускать слово "шаблон" и говорить просто "поток параметров". Когда переменная передается в предложение, она считается входным аргументом и обозначается символом (i). Когда же переменная возвращается из предложения, она является выходным аргументом и обозначается символом (о) в потоке параметров. Разделы Пролог-программы Обычно программа на Прологе состоит из четырех основных программных разделов. К ним относятся: раздел clauses (предложений); раздел predicates (предикатов); раздел domains (доменов); раздел goal (целей). Раздел clauses — это сердце программы; именно в этот раздет записываются факты и правила, которыми будет оперировать Пролог, пытаясь разрешить цель программы. Раздел predicates — это тот, в котором объявляются предикаты и домены (типы) их аргументов. Раздел domains служит для объявления своих доменов (типов данных). Раздел goal — это тот, в который вы помещаете цель программы. Раздел предложений В раздел clauses (предложений) последовательно размещаются факты и правила, составляющие вашу программу. Последовательность предложений, описывающих один предикат и сгруппированных по имени, называется процедурой. Пытаясь разрешить цель, Пролог (начиная с первого предложения раздела clauses) будет просматривать каждый факт и каждое правило, стремясь найти сопоставление. По мере продвижения вниз по разделу clauses, он устанавливает внутренний указатель на первое предложение, являющееся частью пути, ведущего к решению. Если следующее предложение не является частью этого 15 логического пути, то Пролог возвращается к установленному указателю и ищет очередное подходящее сопоставление, перемещая указатель на него (этот процесс называется поиск с возвратом). Синтаксис правил Правила используются в Прологе в случае, когда какой-либо факт зависит от истинности другого факта или группы фактов. Как мы объясняли ранее, в правиле Пролога есть две части заголовок и тело. Ниже представлен обобщенный синтаксис правила: Заголовок :- <Подцель>, <Подцель>, …, <Подцель>. Тело правила состоит из одной или более подцелей. Подцели разделяются запятыми, определяя конъюнкцию, а за последней подцелью правила следует точка. Каждая подцель выполняет вызов другого предиката Пролога, который может быть истинным или ложным. После того как программа осуществила этот вызов, Пролог проверяет истинность вызванного предиката, и если это так, то работа продолжается, но уже со следующей подцелью. Если же в процессе такой работы была достигнута точка, то все правило считается истинным; если хоть одна из подцелей ложна, то все правило ложно. Для успешного разрешения правила Пролог должен разрешить все его подцели и создать последовательный список переменных, должным образом связав их. Если же одна из подцелей ложна Пролог вернется назад для поиска альтернативы предыдущей подцели, а затем вновь двинется вперед, но уже с другими значениями переменных. Этот процесс называется поиск с возвратом. В качестве разделителя заголовка и тела правила Пролог использует знак :-, который читается как "если" (if). Однако if Пролога отличается от if, написанного в других языках, например в Pascal, где условие, содержащееся в операторе if, должно быть указано перед телом оператора, который может быть выполнен. Другими словами: Если ЗАГОЛОВОК истинен, тогда ТЕЛО истинно (или: тогда выполнить ТЕЛО). Данный тип оператора известен как условный оператор если/тогда (if/then).Пролог же использует другую форму логики в таких правилах. Вывод об истинности заголовка правила Пролога делается, если (после того, как) тело этого правила истинно, например, так: ЗАГОЛОВОК истинен, если ТЕЛО-истинно (или:если ТЕЛО может быть выполнено) . Учитывая вышесказанное, правило Пролога соответствует условной форме тогда/если (then/if). Раздел предикатов Если в разделе clauses программы был описан собственный предикат, то вы обязаны объявить его в разделе predicates (предикатов). В результате объявления предиката указывается, к каким доменам (типам) принадлежат аргументы этого предиката. 16 Пролог имеет набор встроенных «системных» предикатов (их не нужно объявлять). Предикаты задают факты и правила. В разделе же predicates все предикаты просто перечисляются с указанием типов (доменов) их аргументов. Как объявить пользовательский предикат Объявление предиката начинается с имени этого предиката, за которым идет открывающая (левая) круглая скобка, после чего следует ноль или больше доменов (типов) аргументов предиката. predicateName(argument_type1 OptionalNamel, argument_type2 OptionalName2,..., argument_typeN OptionalName3) После каждого домена (типа) аргумента следует запятая, а после последнего типа аргумента — закрывающая (правая) скобка. Отметим, что, в отличие от предложений в разделе clauses, декларация предиката не завершается точкой. Доменами (типами) аргументов предиката могут быть либо стандартные домены, либо домены, объявленные в разделе domains. Можно указывать имена аргументов OptionalNameK — это улучшает читаемость программы. Имена предикатов Имя предиката должно начинаться с буквы, за которой может располагаться последовательность букв, цифр и символов подчеркивания. В именах предикатов запрещается использовать пробел, символ минус, звездочку и другие алфавитно-цифровые символы. Аргументы предикатов Аргументы предикатов должны принадлежать доменам, известным Прологу. Эти домены могут быть либо стандартными доменами, либо некоторыми из тех, что вы объявили в разделе доменов. Рассмотрим такой пример. Если предикат my_predicate(symbol, integer) объявлен в разделе predicates следующим образом: predicates my_predicate(symbol, integer) то вам не нужно в разделе domains декларировать домены его аргументов, т. к. symbol и integer — стандартные домены. Однако если этот же предикат вы объявляете так: predicates my_predicate (name,number), то необходимо объявить, что name (символический тип) и number (целый тип) принадлежат к стандартным доменам symbol и integer: domains name = symbol number = integer 17 predicates my_predicate(name, number) Задание типов аргументов при декларации предикатов Объявление доменов аргументов в разделе predicates называется заданием типов аргументов. Предположим, имеется следующая связь объектов: Франк — мужчина, которому 45 лет. Факт Пролога, соответствующий этому предложению естественного языка, может быть следующим: person(frank, male, 45). Для того, чтобы объявить person (человек), как предикат с этими тремя аргументами, вы можете разместить в разделе predicates следующую строку: person(symbol, symbol, unsigned). Здесь для всех трех аргументов использованы стандартные домены. Отныне всякий раз при работе с предикатом person, вы должны передавать ему три аргумента, причем первые два должны быть типа symbol, а третий — типа integer. Если в программе используются только стандартные домены, то нет необходимости использовать раздел domain. Или, предположим, что вы хотите описать предикат, который сообщал бы позицию буквы в алфавите, т.е. цель alphabet_position(Letter, Position) должна вернуть вам Position = 1, если Letter = a, Position = 2, если Letter = b и т.д. Предложения этого предиката могут выглядеть следующим образом: alphabet_position(A_character, N). Если при объявлении предиката используются только стандартные домены, то программе не нужен раздел domains. Предположим, что вы хотите описать предикат так, что цель будет истинна, если A_character является N-м символом алфавита. Предложения этого предиката будут такими: alphabet_position('а', 1). alphabet_position('b', 2). alphabet_position('с', 3). … alphabet_position('z', 26). Вы можете объявить данный предикат следующим образом: predicates alphabet_position(char, unsigned) и тогда вам не будет нужен раздел domains. Если разместить все фрагменты программы вместе, получим: predicates alphabet_position(char, integer) clauses alphabet_position('a', 1). alphabet_position('b', 2). alphabet_position('с', 3). 18 % здесь находятся остальные буквы alphabet_position('z', 26). Ниже представлено несколько простых целей, которые вы можете использовать: alphabet_position('а', 1). alphabet_position(X, 3). alphabet_position('z', What). Встроенные предикаты При разработке программ на Прологе часто используют встроенные предикаты, т. е. предикаты, определяемые автоматически при инициализации интерпретатора Пролога. Встроенные предикаты используются так же, как и определяемые пользователем предикаты. Единственное ограничение - встроенный предикат не может являться головой правила или появляться в факте. Одним из наиболее часто используемых встроенных предикатов является предикат not (отрицание). Этот предикат истинен, если его аргумент ложен, и наоборот. Другим часто используемым встроенным предикатом является = (унификация): =(X, Y). Этот предикат допускает более удобную форму записи X = Y. Значение этого предиката истинно, если термы X и Y удается унифицировать. Иногда бывает полезно использовать предикаты, про которые заранее известно, истинны они или ложны. Для этих целей используют предикаты true и fail. Предикат true всегда истинен, в то время как fail всегда ложен. Встроенный предикат read позволяет считывать термы с клавиатуры. Если при обработке запросов Пролога вы пожелаете получить более подробный вывод, то для этих целей можно использовать предикат write. Аргументом этого предиката может являться любой допустимый терм Пролога. В случае, когда аргументом является переменная, будет напечатано ее значение. Выполнение предиката nl осуществляет перевод строки: последующий вывод начнется с новой строки. Предикат tab выводит количество пробелов, определяемое его аргументом. Раздел доменов Домены позволяют задавать разные имена различным видам данных, которые, в противном случае, будут выглядеть абсолютно одинаково. В Пролог - программах объекты в отношениях (аргументы предикатов) принадлежат доменам, причем это могут быть как стандартные (Таблицы. 1.2, 1.3), так и описанные вами специальные домены. Раздел domains служит двум полезным целям. Во-первых, вы можете задать доменам осмысленные имена, даже если внутренне эти домены аналогичны уже имеющимся стандартным. Во-вторых, объявление специальных доменов используется для описания структур данных, отсутствующих в стандартных доменах. 19 Таблица 1.2 Основные стандартные домены. До Описание Реализация мен sho Короткое, знаковое, Все платформы 16 бит (-32768rt количественное 32767) ush Короткое, Все платформы 16 бит (0-65535) ort беззнаковое, количественное lon Длинное, знаковое, Все платформы 32 бит g количественное (-2147483648-2147483647) ulo Длинное, Все платформы 32 бит ng беззнаковое, (0-4294967295) количественное inte Знаковое, Платформы 16 бит (-32768ger количественное, 32767) имеет платформоПлатформы 32 бит зависимый размер (-2147483648-2147483647) uns Беззнаковое, Платформы 16 бит (0-65535) igned количественное, имеет Платформы 32 бит (0платформо-зависимый 4294967295) размер byt Все платформы 8 бит (0-55) e wor Все платформы 16 бит (0d 65535) dw Все платформы 32 бит ord (0-4294967295) Иногда очень полезно описать новый домен — особенно, когда вы хотите прояснить отдельные части раздела predicates. Объявление собственных доменов, благодаря присваиванию осмысленных имен типам аргументов, помогает документировать описываемые вами предикаты. Данный пример показывает, как объявление доменов поможет документировать предикаты Франк — мужчина, которому 45 лет Используя стандартные домены, вы можете так объявить соответствующий предикат person(symbol, symbol, integer). В большинстве случаев такое объявление будет работать очень хорошо. Однако предположим, что несколько месяцев спустя после написания программы, вы решили скорректировать ее. Есть опасение, что подобное объявление этого предиката абсолютно ничего вам не скажет. И напротив, декларация этого же предиката, представленная ниже, поможет вам разобраться в том, что же представляют собой аргументы данного предиката. domains name, sex = symbol age = integer 20 predicates person(name, sex, age) Таблица 1.3 Дополнительные стандартные домены Д Описание омен C Символ, реализуемый как беззнаковый byte. Синтаксически это har символ заключенный между двумя одиночными кавычками: 'а' Число с плавающей запятой, реализуемое как 8 байт в eal соответствии с соглашением IEEE; эквивалентен типу double в С. Примеры действительных чисел (real): 42705…9999…86.72 9111.929437521е238…79.83е+21 Здесь 79.83е+21 означает 79.83x1021 , как и в других языках. Допустимый диапазон чисел: от 1x10-307 до 1x10+308(от 1е-307 до 1е+308). При необходимости, целые автоматически преобразуются в real st Последовательность символов, реализуемых как указатель на ring байтовый массив, завершаемый нулем как в С. Для строк допускается два формата: 1.Последовательность букв, цифр и символов подчеркивания, причем первый символ должен быть строчной буквой. 2.Последовательность символов, заключенных в двойные кавычки. Примеры строк: telephone_number “railway ticket” “Dorid Inc” Строки, которые вы пишете в программе, могут достигать длины в 255 символов sy Последовательность символов, реализуемых как указатель на mbol вход в таблице идентификаторов, хранящей строки идентификаторов. Синтаксис — как для строк Синтаксически значение, принадлежащее одному из целочисленных доменов, записывается как последовательность цифр, которой в случае знакового домена может предшествовать не отделенный от нее пробелом знак минус. Домены типов byte, word и dword наиболее удобны при работе с машинными числами. В основном используются типы integer и unsigned, а также short и long (и их беззнаковые аналоги) для более специализированных приложений. В объявлениях доменов ключевые слова signed и unsigned могут использоваться вместе со стандартными доменами типов byte, word и dword для построения новых базовых доменов. Так: domains 18 = signed byte создает новый базовый домен в диапазоне от -128 до +127. R 21 Раздел цели По существу, раздел goal (цели) аналогичен телу правила: это просто список подцелей. Цель отличается от правила лишь следующим: за ключевым словом goal не следует :- ; при запуске программы Пролог автоматически выполняет цель. Если все подцели в разделе goal истинны, — программа завершается успешно. Если же какая-то подцель из раздела goal ложна, то считается, что программа завершается неуспешно. Работа с числами на Прологе В языке Пролог имеется ряд встроенных функций для вычисления арифметических выражений, некоторые из которых перечислены в таблице1.4. Таблица 1.4. Основные арифметические функции X+Y Сумма X и Y X-Y Разность X и Y X*Y Произведение X и Y X/Y Деление X на Y X mod Y Остаток от деления X на Y X // Y Деление нацело X на Y X ** Y Возведение X в степень Y -X Смена знака X abs(X) Абсолютная величина числа X max(X,Y) Большее из чисел X и Y min(X,Y) Меньшее из чисел X и Y sqrt(X) Квадратный корень из X random(In Случайное целое число в диапазоне от 0 t) до Int sin(X) / Синус X / Косинус X cos(X) tan(X) Тангенс X log(X) Натуральный логарифм (ln) числа X log10(X) Десятичный логарифм (lg) числа X 3.14159 (приближенное значение числа pi ) е 2.71828 (приближенное значение числа е) Для вычисления арифметических выражений в Прологе используется встроенный бинарный оператор is, который интерпретирует правый терм как арифметическое выражение, после чего унифицирует (если возможно) результат 22 вычисления с левым термом (обычно с переменной). Приоритет выполнения арифметических операций является традиционным. Круглые скобки используются для изменения порядка вычислений. Для сравнения арифметических выражений используется ряд операторов. Цель X > Y (больше) будет успешна, если выражение X будет соответствовать большему числу, чем выражение Y. Аналогично используются операторы < (меньше), =< (меньше или равно), >= (больше или равно), =\= (не равно) и =:= (арифметически равный). Различия между операторами =:= и = очень существенны. Первый оператор сравнивает значения арифметических выражений, тогда как последний пытается унифицировать их. Пример решения логической задачи на Прологе Рассмотрим такую задачу: Для каждого из четырех сезонов года купили по паре туфель разного цвета – красного, синего, зеленого, желтого. Какую-то пару туфель носили зимой, какую-то - летом, какую-то – осенью, какую-то – весной. Пару туфель какого цвета носили осенью, если известно, что: 1) желтые туфли носили не осенью и не зимой; 2) летом носили зеленые туфли; 3) осенью носили пару не красных туфель; 4) синие туфли носили не весной. Аналитическим способом логические задачи могут решаться заполнением таблиц. По условиям данной задачи, зеленые туфли носили летом – следует поставить символ '+' в соответствующей клетке; зеленые туфли носили не зимой, не весной и не осенью, желтые туфли носили не осенью и не зимой, красные туфли носили не осенью, синие туфли носили не весной – следует поставить символ '-' в соответствующих клетках. Между множеством туфель и множеством времен года должно быть установлено взаимнооднозначное соответствие. В каждой строке и каждом столбце должен быть только один знак '+'. З Л В О има ето есна сень Зеленые туфли + Красные туфли + Синие туфли + Желтые туфли + Из построенной таблицы следует, что осенью носили синие туфли. 23 Листинг программы на Прологе: % раздел доменов domains season=winter;summer;autumn;spring color=green;red;blue;yellow /* раздел предикатов */ predicates shoes(color). wear(season,color). notwear(season,color). % раздел правил clauses shoes(green). shoes(red). shoes(blue). shoes(yellow). notwear(autumn,yellow). notwear(winter,yellow). notwear(autumn,red). notwear(spring,blue). wear(summer,green). wear(autumn,X) :- shoes(X), not(notwear(autumn,X)), not(wear(summer,X)), nl,write("Autumn wear ",X," shoes"),nl. /* раздел цели */ goal wear(autumn,X). 24 Лабораторная работа №2 Тема: Программирование структур данных на языке ПРОЛОГ. Цель: Изучение принципов логического программирования таких структур данных, как списки и бинарные деревья, на языке ПРОЛОГ. Задание В соответствии с вариантом задания написать программу поиска решения задачи на языке ПРОЛОГ с использованием структур данных. Теоретическая часть Рассмотрим два вида широко используемых структур данных: списки и деревья. 1 Списки 1.1 Понятие списка В Прологе список — это объект, который содержит конечное число других объектов. Списки можно грубо сравнить с массивами в других языках, но, в отличие от массивов, для списков нет необходимости заранее объявлять их размер. Конечно, есть другие способы объединить несколько объектов в один. Если число объектов заранее известно, то вы можете сделать их аргументами одной составной структуры данных. Если число объектов не определено, то можно использовать рекурсивную составную структуру данных, такую как дерево. Но работать со списками обычно легче. Список, содержащий числа 1, 2 и 3, записывается так: [1, 2, 3] Каждая составляющая списка называется элементом. Чтобы оформить списочную структуру данных, надо отделить элементы списка запятыми и заключить их в квадратные скобки. 1.2 Объявление списков Чтобы объявить домен для списка целых, надо использовать декларацию домена, такую как: domains Integerlist = integer* Символ (*) означает "список чего-либо"; таким образом, integer* означает "список целых". Элементы списка могут быть любыми, включая другие списки. Однако все его элементы должны принадлежать одному домену. Декларация домена для элементов должна быть следующего вида: domains elementslist = elements* elements = .... 25 Здесь elements имеют единый тип (например: integer, real или symbol) или являются набором отличных друг от друга элементов, отмеченных разными функторами. 1.3 Головы и хвосты Список является рекурсивным составным объектом. Он состоит из двух частей - головы, которая является первым элементом, и хвоста, который является списком, включающим все последующие элементы. Хвост списка — всегда список, голове списка — всегда элемент. Например: голова [а, b, с] есть а хвост [а, b, с] есть [b, с] Что происходит, когда вы доходите до одноэлементного списка? Ответ таков: голова [с] есть с хвост [с] есть [] Если выбирать первый элемент списка достаточное количество раз, вы обязательно дойдете до пустого списка [ ]. Пустой список нельзя разделить на голову и хвост. 1.4 Работа со списками В Прологе есть способ явно отделить голову от хвоста. Вместо разделения элементов запятыми, это можно сделать вертикальной чертой "|". Например: [а, b, с] эквивалентно [а | [b, с]] и. продолжая процесс, [а | [b, с] ] эквивалентно [а | [b] [с] ] ], что эквивалентно [а | [b | [с | [ ] ] ] ] Можно использовать оба вида разделителей в одном и том же списке при условии, что вертикальная черта есть последний разделитель. При желании можно набрать [a, b, с, d] как [а, b | [с, d]]. В таблице 2.1 приведено несколько примеров на присвоение в списках. Таблица 2.1 Присвоение в списках Список 1 Список 2 Присвоение переменным [X, У, Z] [7] [1, 2, 3, 4] [эгберт, ест, мороженое] [X | У] Х=эгберг, У=ест, Z=мороженое Х=7, У=[] [X, У | Z] Х=1, У=2, Z=[3,4] 1.4.1 Печать списков Если нужно напечатать элементы списка, это делается рекурсивно: domains list=integer* predicates write_a_list(list) clauses write_a_list([]). % если список пустой – ничего не делать 26 write_a_list([H|T]) :- % присвоить H – голова, T – хвост, затем… write(H), nl, write_a_list(T). goal write_a_list([1,2,3]). Вот два целевых утверждения write_a_list, описанные на обычном языке: Печатать пустой список — значит ничего не делать. Иначе, печатать список — означает печатать его голову (которая является одним элементом), затем печатать его хвост (список). При первом просмотре целевое утверждение таково: write_a_list([1,2,3]). Оно удовлетворяет второму предложению при H = 1и Т = [2, 3]. Компьютер напечатает 1 и вызовет рекурсивно write_a_list: write_a_list([2,3]). % Это write_a_list (Т) Этот рекурсивный вызов удовлетворяет второму предложению. На этот раз Н = 2 и Т = [3], так что компьютер печатает 2 и снова рекурсивно вызывает write_a_list с целевым утверждением write_a_list([3]) . Итак, какому предложению подходит это целевое утверждение? Вспомним, что, хотя список [3] имеет всего один элемент, у него есть голова 3 и хвост []. Следовательно, целевое утверждение снова подходит под второе предложение с Н = 3 и Т = [ ]. Программа печатает 3 и делает рекурсивный вызов: write_a_list([]). Теперь видно, что этому целевому утверждению подходит первое предложение. Второе предложение не подходит, т. к. [] нельзя разделить на голову и хвост. Так что, если бы не было первого предложения, целевое утверждение было бы невыполнимым. Но первое предложение подходит, и целевое утверждение выполняется без дальнейших действий. 1.4.2 Подсчет элементов списка Рассмотрим, как можно определить число элементов в списке. Что такое длина списка? Вот простое логическое определение: Длина [] — 0. Длина любого другого списка — 1 плюс длина его хвоста. Можно ли применить это? В Прологе — да. Для этого нужны два предложения: domains list=integer* % или любой другой тип predicates length_of(list,integer) clauses length_of([],0). length_of([_,T],L):- length_of(T,TailLength), L = TailLength+1. Посмотрим сначала на второе предложение. Действительно, [_|Т] можно сопоставить любому непустому списку, с присвоением T хвоста списка. 27 Значение головы не важно, главное, что оно есть, и компьютер может посчитать его за один элемент. Таким образом, целевое утверждение length_of([1,2,3],L). подходит второму предложению при T=[2, 3]. Следующим шагом будет подсчет длины T. Когда это будет сделано (не важно как), TailLength будет иметь значение 2, и компьютер добавит к нему 1 и затем присвоит L значение 3. Итак, как компьютер выполнит промежуточный шаг? Это шаг, в котором определяется длина [2, 3] при выполнении целевого утверждения length_of([2,3],TailLength). Другими словами, length_of вызывает сама себя рекурсивно. Это целевое утверждение подходит второму предложению с присвоением: - [3] из целевого утверждения присваивается T в предложении; - ТаilLenght из целевого утверждения присваивается L в предложении. Напомним, что ТаilLength в целевом утверждении не совпадает с ТаilLength в предложении, потому что каждый рекурсивный вызов в правиле имеет свой собственный набор переменных. Итак, целевое утверждение состоит в том, чтобы найти длину [3], т. е. 1, а затем добавить 1 к длине [2, 3], т. е. к 2, и т. д. Таким образом, length_of вызывает сама себя рекурсивно, чтобы получить длину списка [3]. Хвост [3] - [], так что T будет присвоен [], а целевое утверждение будет состоять в том, чтобы найти длину [] и, добавив к ней 1, получить длину [3]. На сей раз все просто. Целевое утверждение length_of([],TailLength). удовлетворяет первому предложению, которое присвоит 0 переменной TailLength. Пролог добавит к нему 1 и получит длину [3], затем вернется к вызывающемe предложению. Оно в свою очередь снова добавит 1, получит длину [2, 3] и вернется в вызывающее его предложение. Это начальное предложение снова добавит 1 и получит длину [1, 2, 3]. 1.4.3 Преобразование списков Иногда необходимо преобразовать один список в другой. Это делается поэлементно, заменяя каждый элемент вычисленным значением. Вот пример: программа добавит 1 к каждому элементу числового списка. domains list=integer* predicates add1(list,list) clauses add1([],[]). % граничное условие add1([Head | Tail], [Head1 | Tail1]):- % отделить голову списка Head1=Head+1, % добавить 1 к 1-му элементу add1(Tail, Tail1). % вызвать элемент из остатка списка goal add1([1,2,3,4],NewList). 28 Переведя это на естественный язык, получим: Чтобы добавить 1 ко всем элементам пустого списка, надо создать другой пустой список. Чтобы добавить 1 ко всем элементам любого непустого списка, надо добавить 1 к голове и сделать полученный элемент головой результирующего списка, затем добавить 1 к каждому элементу хвоста списка и сделать это хвостом результата. Введите программу и запустите ее с целевым утверждением add1([1,2,3,4],NewList). Пролог выдаст результат: NewList=[2,3,4,5] 1 Solution Конечно, не всегда нужно заменять каждый элемент. Далее следует пример программы, которая просматривает список из чисел и делает него копию, отбрасывая отрицательные числа. domains list=integer* predicates discard_negatives(list,list) clauses discard_negatives ([],[]). discard_negatives ([H | T], ProcessedTail):H<0, % если Н отрицательно, то пропустить !, discard_negatives (T, ProcessedTail). discard_negatives ([H | T], [H | ProcessedTail]):discard_negatives (T, ProcessedTail). К примеру, целевое утверждение discard_negatives ([2, -45, 3, 468], X) получит X = [2, 3, 468]. Далее приведем предикат, который копирует элементы списка, заставляя каждый элемент появляться дважды: doubletalk ([],[]). doubletalk ([H | T], [H, H | DoubledTail]):- doubletalk (T, DoubledTail). 1.4.4 Принадлежность к списку Предположим, что у вас есть список имен John, Leonard, Eric и Franc, и вы хотите проверить, имеется ли заданное имя в этом списке. Другими словами, вам нужно выяснить отношение "принадлежность" между двумя аргументами — именем и списком имен. Это выражается предикатом member(name,namelist) . % "name" принадлежит "namelist" В приведенной ниже программе первое предложение проверяет голову списка. Если голова списка совпадает с именем, которое вы ищете, то можно заключить, что name (имя) принадлежит списку. Так как хвост списка не представляет интереса, он обозначается анонимной переменной. По первому предложению целевое утверждение member(john, [john, leonard, eric, franc ]) будет выполнено. 29 domains namelist=name* name=symbol predicates member(name,namelist) clauses member(Name,[Name|_]). member(Name,[_|Tail]):- member(Name,Tail). Если голова списка не совпадает с Name, то нужно проверить, можно ли Name найти в хвосте списка. На обычном языке: Name принадлежит списку, если Name есть первый элемент списка, или Name принадлежит списку, если Name принадлежит хвосту. Второе предложение member, выражающее отношение принадлежности, в Прологе выглядит так: member(Name,[_|Tail]):- member(Name,Tail). 1.4.5 Объединение списков Из вышеприведенной программы предикат member работает двумя способами. Рассмотрим его предложения еще раз: member(Name,[Name|_]). member(Name,[_|Tail]):- member(Name,Tail). На эти предложения можно смотреть с двух различных точек зрения: декларативной и процедурной. С декларативной точки зрения предложения сообщают: Name принадлежит списку, если голова совпадает с Name; если нет, то Name принадлежит списку, если оно принадлежит его хвосту. С процедурной точки зрения эти два предложения можно трактовать так: Чтобы найти элемент списка, надо найти его голову; иначе надо найти элемент в хвосте. Эти две точки зрения соотносятся с целевым утверждением: member(2,[1,2,3,4]). и member(Х, [1, 2, 3, 4]). В результате, первое целевое утверждение "просит" выяснить, верно ли утверждение, второе, — найти всех членов списка [1,2,3,4]. Предикат member одинаков в обоих случаях, но его поведение может быть рассмотрено с разных точек зрения. 1.4.5.1 Рекурсия с процедурной точки зрения Особенность Пролога состоит в том, что часто, когда вы задаете предложения для предиката с одной точки зрения, они будут выполнены с другой. Чтобы увидеть эту двойственность, создадим в следующем примере предикат для присоединения одного списка к другому. Определим предикат append с тремя аргументами: 30 append(List1,List2,List3) Он объединяет List1 и List2 и создает ListЗ. Еще раз воспользуемся рекурсией (на этот раз с процедурной точки зрения). Если List1 пустой, то результатом объединения List1 и List2 останется List2. На Прологе это будет выглядеть так: append([],List2,List2) . Если List1 не пустой, то можно объединить List1 и List2 для формирования ListЗ, сделав голову List1 головой List3. (В следующем утверждении переменная H используется как голова для List1 и для List3.) Хвост List3 — это L3, он состоит из объединения остатка List1 (т. е. L1) и всего List2. То есть: append([H|L1],List2,[H|L3]) :append(L1,List2,L3). Предикат append выполняется следующим образом: пока List1 не пустой, рекурсивное предложение передает по одному элементу в List3. Когда List1 станет пустым, первое предложение унифицирует List2 с полученным List3. 1.4.5.2 Предикат может иметь несколько вариантов использования Рассматривая append с декларативной точки зрения, вы определили отношение между тремя списками. Однако это отношение сохранится, даже если List1 и List 3 известны, а List 2 — нет. Оно также справедливо, если известен только List3. Например, чтобы определить, какие из двух списков можно объединить для получения известного списка, надо составить целевое утверждение такого вида: append(L1, L2, [1, 2, 4]). По этому целевому утверждению Пролог найдет следующие решения: L1=[], L2=[1,2,4] L1=[1], L2=[2,4] L1=[1,2], L2=[4] L1=[1,2,4], L2=[] 4 Solutions Можно также применить append, чтобы определить, какой список можно подсоединить к [3,4] для получения списка [1,2,3,4]. Запустите целевое утверждение append(L1, [3,4], [1,2,3,4]). Пролог найдет решение: L1=[1,2]. Предикат append определил отношение между входным набором и выходным набором таким образом, что отношение применимо в обоих направлениях. Задавая такое отношение, вы спрашиваете: Который выход соответствует данному входу? или Который вход соответствует данному выходу? Состояние аргументов при вызове предиката называется потоком параметров. Аргумент, который присваивается или назначается в момент вызова, называется входным аргументом и обозначается буквой i; а свободный аргумент — это выходной аргумент, обозначается буквой o. 31 У предиката append есть возможность работать с разными потоками параметров, в зависимости от того, какие исходные данные вы ему дадите. Однако не для всех предикатов имеется возможность быть вызванными с различными потоками параметров. Если предложение Пролога может быть использовано с различными потоками параметров, оно называется обратимым предложением. Когда вы записываете собственные предложения в Прологе, помните, что обратимые предложения имеют дополнительные преимущества, и их создание добавляет "мощности" предикатам. 1.5 Поиск всех решений для цели сразу Рекурсия в отличие от поиска с возвратом передает информацию (через параметры) от одного рекурсивного вызова к следующему. Поэтому рекурсивная процедура может хранить память о промежуточных результатах или счетчиках по мере того, как она выполняется. Но есть одна вещь, которую поиск с возвратом может делать, а рекурсия — нет. Это поиск всех альтернативных решений в целевом утверждении. Может оказаться, что вам нужны все решения для целевого утверждения, и они необходимы все сразу, как часть единой сложной составной структуры данных. Встроенный предикат findall использует целевые утверждения в качестве одного из своих аргументов и собирает все решения для этого целевого утверждения в единый список. У предиката findall три аргумента: 1) VarName (имя переменной) — определяет параметр, который необходимо собрать в список; 2) myPredicate (мой предикат) — определяет предикат, из которого надо собрать значения; 3) ListParam (список параметров) — содержит список значений, собранных методом поиска с возвратом. Заметьте, что должен быть определенный пользователем тип, которому принадлежат значения ListParam. Нижеприведенная программа использует findall для печати среднего возраста группы людей. domains name, address=string age=integer list=age* predicates person(name, address, age) sumlist(list, age, integer) clauses sumlist([],0,0). sumlist([H|T],Sum,N):sumlist(T,S1,N1), Sum=H+S1, N=1+N1. person(“Sherlock Holms”, “22B Baker Street”, 42). person(“Pete Spiers”, “Apt. 22, 21st Street”, 36). person(“Marry Darrow”, “Suite 2, Omega Home”, 51). 32 goal findall(Age, person (_,_,Age),L), sumlist(L,Sum,N), Ave=Sum/N, write(“Average=”,Ave), nl. Предложение findall в этой программе создает список L, в котором собраны все возрасты, полученные из предиката person. Если бы вы захотели собрать список из всех людей, которым 42 года, то вам следовало бы выполнить следующее подцелевое утверждение: findall(Who, person(Who,_,42),List) Но эта подцель требует от программы, чтобы та содержала объявление домена для результирующего списка: slist=string* 1.6 Составные списки Список целых может быть объявлен просто: integerlist=integer* Это же справедливо и для списка действительных чисел, списка идентификаторов или списка строк. Часто бывает важно иметь внутри одного списка комбинацию элементов, принадлежащих разным типам. Составные списки — это списки, в которых используется более чем один тип элементов. Для работы со списками из разнотипных элементов нужны специальные декларации. Для создания списка, который мог бы хранить различные типы элементов, в Прологе необходимо использовать функторы, потому что домен может содержать более одного типа данных в качестве аргументов для функторов. Пример объявления доменов для списка, который может содержать символы, целые, строки или списки: domains llist=l(list); i(integer); c(char); s(string) % функторы l, i, c, s list=llist* Например, составной список может быть такой: [i(2), i(9), l([s(“food”), s(“goo”)]), s(“new”)] Резюме Списки — это объекты, которые имеют произвольное число элементов. Вы объявляете их, добавляя * (звездочку) после имени домена (стандартного или предварительно декларированного в программе). Список — это рекурсивный составной объект, состоящий из головы и хвоста. Голова — это первый элемент списка, а хвост — это все остальные элементы списка (без первого элемента). Хвост списка — всегда список. Голова списка — всегда элемент. Список может иметь ноль или больше элементов; пустой список записывается как []. Для разделения головы и хвоста в списке используются разделители (запятая, [, ] и |). 33 Все элементы в списке должны принадлежать одному и тому же домену, поэтому необходимо пользоваться функторами для создания списка, который хранит элементы разных доменов. Элементы списка могут быть любыми, включая и другие списки. Работа со списком состоит из рекурсивного отделения его головы (и, обычно, производится некоторая ее обработка), до тех пор, пока список не станет пустым. Классические предикаты Пролога member и append дают возможность проверить, содержится ли элемент в списке и содержится ли один список в другом (или добавлен ли один список к другому) соответственно. Поток параметров — это состояние аргументов предиката при его вызове; они могут быть входными параметрами (i) — которым что-либо присвоено или они связаны, или выходными параметрами (о) — свободными. Предикат findall — это встроенный предикат, который использует целевое утверждение, как один из своих аргументов, и собирает все решения для этого целевого утверждения в один список. 2 Деревья как типы данных Структурой вводимого нами типа данных является дерево (рис2.1). Каждая ветвь дерева сама является деревом, поэтому структура рекурсивна. Рис. 2.1 Часть фамильного дерева Рекурсивные типы популяризировались Никлаусом Виртом, изобретателем языка программирования Pascal. Он не применял в Pascal рекурсивные типы, но определил, какими они должны быть. Но если бы в Pascal они все-таки были, то можно было бы определить дерево наподобие следующего: tree = record /* Некорректно в Pascal! */ name: string[80]; left, right: tree end. На естественный язык этот фрагмент переводится так: «Дерево состоит из имени (Name), которое есть строка (string), а также левого и правого поддеревьев, которые тоже являются деревьями». Однако в Pascal можно написать только следующим образом: 34 treeptr = ^tree; tree = record name: string[80]; left, right: treeptr end. Заметьте существенное различие: этот фрагмент имеет дело с представлением дерева в памяти, а не с собственно структурой дерева. Он обращается с деревом как с объектом, состоящим из узлов, каждый из которых содержит некоторые данные и указатели на два других узла. Пролог позволяет определить действительно рекурсивные типы, в которых указатели создаются и обрабатываются автоматически. Например, можно определить дерево следующим образом: domains treetype = tree(string, treetype, treetype) Эта декларация говорит о том, что дерево записывается как функтор tree, аргументами которого являются строка и два других дерева. Но это не совсем удовлетворительная декларация, т. к. нет способа закончить рекурсию. В действительности дерево не может распространяться до бесконечности. Некоторые узлы не имеют связей с последующими деревьями. В Pascal это можно выразить, присвоив некоторым указателям специальное нулевое значение, но в Прологе нет доступа к указателям. Решение состоит в том, чтобы определить два типа деревьев – обычное и пустое. Это достигается тем, что дерево может иметь один из двух функторов tree с тремя аргументами или empty без аргументов. domains treetype = tree(string, treetype, treetype); empty Заметьте, что названия tree (функтор, у которого три аргумента) и empty (функтор без аргументов) создаются программистом, и ни одному из них нет предопределенного в Прологе значения. С тем же успехом можно использовать ххх и ууу. Вот как дерево, представленное на рисунке 1, будет описано в Пролог-программе: tree("Cathy", tree("Michael", tree("Charles", empty, empty), tree("Hazel", empty, empty)), tree ("Melody", tree("Jim", empty, empty), tree("Eleanor", empty, empty))) Для удобства чтения программа имеет подразделы. Но в Прологе не требуется такого подразделения, и деревья, при нормальной записи, не требуется подразделять. Точно такая же структура будет составлена другим способом: tree("Cathy", tree("Michael", tree("Charles", empty, empty), tree("Hazel", empty, empty)), tree("Melody", tree("Jim", empty, empty), tree("Eleanor", empty, empty))) Заметьте, что это не предложение Пролога, а лишь сложная структура данных. 35 2.1 Обход дерева Одной из наиболее часто осуществляемых операций с деревом является исследование всех узлов и обработка их некоторым образом, либо поиск некоторого значения, либо сбор всех значений. Эти процедуры известны как обход дерева. Основной алгоритм для этого следующий: 1. Если дерево пусто, то ничего не делать. 2. Иначе, обработать текущее значение, затем перейти на левое поддерево, затем перейти на правое поддерево. Как и само дерево, алгоритм является рекурсивным: он обрабатывает левое и правое поддеревья так же, как и исходное дерево. В Прологе он выражается двумя предложениями: одно для пустого, а другое для непустого дерева. traverse (empty). % ничего не делать traverse (tree (X, Y, Z)) :do_something_with_X, traverse(Y), traverse(Z). Рис. 2.2 - Обход дерева "сначала – вглубь" Этот алгоритм известен как поиск "в глубину", т.к. он спускается по каждой ветви вниз, насколько возможно, прежде чем вернуться вверх для обхода другой ветви. Рассмотрим программу, которая обходит дерево и печатает все элементы, которые ей попадаются. domains treetype = tree(string, treetype, treetype); empty() predicates traverse(treetype) clauses traverse(empty). traverse(tree(Name, Left, Right)) :write(Name,'\n'), traverse(Left), traverse(Right). 36 goal traverse(tree("Cathy", tree("Michael", tree("Charles", empty, empty), tree("Hazel", empty, empty)), tree("Melody", tree("Jim", empty, empty), tree("Eleanor", empty, empty)))). Поиск "сначала – вглубь" очень похож на способ, которым Пролог осуществляет поиск в базе знаний. Он организует предложения в дерево и проходит каждое поддерево, пока предложения не закончатся. Вы могли бы описать дерево посредством набора правил Пролога, таких как: father_of("Cathy", "Michael"). mother_of("Cathy", "Melody"). father_of("Michael", "Charles"). mother_of("Michael", "Hazel"). … Это было бы предпочтительнее, если бы дерево предназначалось только для выражения родственных связей между людьми. Но такой вид описания делает невозможным обработку всего дерева как единой, сложной структуры данных. Сложные структуры данных бывают весьма полезными, т. к. они упрощают сложные вычислительные задачи. 2.2 Создание дерева Один из способов создания дерева – это вложенная структура из функторов и аргументов, как в предыдущем примере. Однако в общем случае Пролог создает дерево путем вычисления. На каждом шаге пустое поддерево заменяется непустым в процессе унификации (сопоставления по аргументам). Создание дерева из одного узла путем присвоения обычных данных тривиально: create_tree(N, tree(N, empty, empty)). Здесь говорится: "Если N – данное, то tree(N, empty, empty) – это дерево из одного узла, содержащее его". Построение структуры дерева так же просто. Следующей процедуре нужны три дерева в качестве аргументов. Она вставляет первое дерево в качестве левого поддерева во второе дерево, и результат этого присваивает третьему дереву: insert_left(X, tree(А, _, В), tree(А, X, В)). Заметьте, что в этом предложении нет тела, т. е. нет четких шагов при его выполнении. Все, что должен сделать компьютер, – это соединить аргументы друг с другом в правильном порядке, – и работа закончена. Предположим, что вы хотите вставить tree("Michael", empty, empty) в качестве левого поддерева для tree("Cathy", empty, empty). Чтобы это сделать, надо выполнить целевое утверждение: insert_left(tree("Michael", empty, empty), tree("Cathy", empty, empty), T) 37 Тогда Т примет значение: tree("Cathy", tree("Michael", empty, empty), empty). Это дает способ построения дерева шаг за шагом. 2.3 Бинарные поисковые деревья Итак, мы использовали дерево для представления отношений между элементами. Это, конечно, не самый лучший вид использования деревьев, т. к. предложения Пролога могут выполнить такую же работу. Но деревья можно использовать и иначе. В деревьях имеется возможность хранить данные так, что их можно быстро отыскать. Дерево, построенное для такой цели, называется поисковым деревом. С точки зрения пользователя, сама структура дерева не несет информации, дерево – это более быстрая альтернатива списку или массиву. Вспомним, что при обходе обычного дерева вы рассматриваете текущий узел, а затем оба его поддерева. Чтобы найти определенное значение, вы должны рассмотреть каждый узел всего дерева. Бинарное поисковое дерево строится таким образом, что, глядя на любой узел, можно предсказать, в каком из его узлов находится заданное значение. Это делается заданием отношения порядка между значениями, таким как алфавитный или пронумерованный порядок. Значения в левом поддереве предшествуют значению в текущем узле, а в правом – следуют после значения в текущем узле (рис. 2.3). Заметьте, что те же имена, установленные в ином порядке, дадут другое дерево, и хотя в дереве десять имен, любое из них можно найти максимум за пять шагов. Рис. 2.3 - Бинарное поисковое дерево Как только вы посмотрите во время поиска на узел бинарного поискового дерева, вы можете исключить из рассмотрения половину оставшихся узлов и провести поиск очень быстро. Если теперь размер дерева увеличить вдвое, то для поиска потребуется только один дополнительный шаг. Чтобы построить дерево, нужно начать с пустого дерева и добавлять к нему значения одно за другим. Процедура добавления значения такая же, как при 38 поиске: необходимо просто найти место, где это значение должно находиться, и вставить его туда. Этот алгоритм заключается в следующем: 1. Если текущий узел есть пустое дерево, то вставить в него значение. 2. Иначе, сравнить значение, которое необходимо вставить, со значением в текущем узле. Вставить значение в левое или правое поддерево, в зависимости от результата сравнения. В Прологе этому алгоритму нужно три предложения — по одному на каждый случай. Первое предложение таково: insert(Newltem, empty, tree(Newltem, empty, empty) :- !. На естественный язык эта запись переводится так: "Результатом вставки Newltem (нового значения) в empty (пустое дерево) будет дерево tree(Newltem, empty, empty)". Восклицательный знак (! – отсечение) означает, что если это предложение можно успешно применить, то другие предложения проверять не надо. Второе и третье предложения осуществляют вставку в непустые деревья: insert(Newltem, tree(Element, Left, Right), tree(Element, NewLeft, Right) :Newltem < Element, !, insert(Newltem, Left, NewLeft). insert(Newltem, tree(Element, Left, Right), tree(Element, Left, NewRight) :insert(Newltem, Right, NewRight). Если Newltem<Element, то вы вставляете его в левое поддерево, а если иначе, то вставляете его в правое поддерево. Заметьте, как много работы мы выполняем проверкой соответствия аргументов в голове правила. 2.4 Сортировка на основе дерева После того, как дерево построено, можно легко переставить все его элементы в алфавитном порядке. Алгоритм для этого – вновь вариант поиска "сначала – вглубь": 1. Если дерево пустое, то ничего не делать. 2. Иначе, переставить все элементы левого поддерева, потом текущий элемент, затем все элементы правого поддерева. Или на Прологе: retrieve_all(empty). % Ничего не делать retrieve_all(tree(Item, Left, Right)) :retrieve_all(Left), do_something_to (Item), retrieve_all(Right). Сортировку следующих друг за другом значений можно выполнить, вставляя их в дерево, и затем переставляя их по порядку. Нижеприведенная программа применяет описанный способ для расстановки по алфавиту вводимых символьных значений. В этом примере используются некоторые стандартные предикаты. domains chartree = tree(char, chartree, chartree); end predicates do(chartree) 39 action(char, chartree, chartree) create_tree(chartree, chartree) insert(char, chartree, chartree) write_tree(chartree) repeat clauses do(Tree):repeat, nl, write("***************************************************"), nl, write("Enter 1 to update tree\n"), write("Enter 2 to show tree\n"), write("Enter 7 to exit\n") write("***************************************************"), nl, write("Enter number – "), readchar(X), nl, action(X, Tree, NewTree ), do(NewTree). action('1',Tree,NewTree):write("Enter characters or # to end: "), create_Tree(Tree, NewTree). action('2',Tree,Tree):write_Tree(Tree), write("\nPress a key to continue"), readchar(_),nl. action('7', _, end):exit. create_Tree(Tree, NewTree):readchar(C), C<>' # ', !, write(C, " "), insert(C, Tree, TempTree), create_Tree(TempTree, NewTree). create_Tree(Tree, Tree). insert(New,end,tree(New,end,end)):!. insert(New,tree(Element,Left,Right),tree(Element,NewLeft,Right)):New<Element, !, insert(New,Left,NewLeft). insert(New,tree(Element,Left,Right),tree(Element,Left,NewRight)):insert(New,Right,NewRight). write_Tree(end). write_Tree(tree(Item,Left,Right)):40 write_Tree(Left), write(Item, " "), write_Tree(Right). repeat. repeat:-repeat. goal write("************* Character tree sort *************"),nl, do(end). Варианты заданий на работу Примечание: Во всех вариантах в задаче № 2 используется дерево, изображенное на рис.2.4. 3 2 1 5 2 8 9 Рис.2.4 – Дерево Вариант 1 1 Определить максимальный элемент списка чисел [5, 2, 9, 8, 3, 1]. 2 Определить число вхождений элемента 2 в дерево. Вариант 2 1 Найти второй по величине элемент списка чисел [1, 5, 2, 6, 3]. 2 Вычислить среднее арифметическое всех элементов дерева. Вариант 3 1 Сформировать новый список из тех элементов списка [7, 1, 3, 8, 5, 4, 2], которые стоят на нечетных позициях. 2 Определить, входят ли элементы 7 и 8 в дерево. Вариант 4 1 Подсчитать количество целых чисел, входящих в список [1, 2, 2, 3, 4, 5, 5, 6, 6] только один раз. Список упорядочен. 2 Найти наибольший элемент дерева. Вариант 5 1 Найти сумму максимального и минимального элементов списка [8, 4, 2, 6, 15]. 2 Найти в дереве длину (число ветвей) пути от корня до ближайшей вершины с элементом n. 41 Вариант 6 1 Определить количество элементов списка кратных 2. Список [2, 3, 4, 6, 9, 8, 12] 2 Вывести все элементы дерева по уровням: сначала из корня дерева, затем (слева направо) – из вершин, дочерних по отношению к корню, затем (также слева направо) – из вершин, дочерних по отношению к этим вершинам, и т.д. Вариант 7 1 Найти сумму элементов данного списка, которые стоят на четных позициях. Список [5, 1, 3, 7, 8, 4]. 2 Найти число вершин на n-ом уровне дерева (корень считать вершиной 0-го уровня). Вариант 8 1 Перенести в конец списка [1, 2, 3, 4, 5] все четные элементы. 2 Определить максимальную глубину дерева, т.е. число ветвей в самом длинном из путей от корня дерева до листьев. Вариант 9 1 Получить новый список из двух следующим образом: в начало нового списка поместить все четные элементы списка [5, 2, 4, 8], а в конец – все нечетные элементы списка [1, 2, 5, 8, 7]. 2 Вывести элементы из всех листьев дерева. Вариант 10 1 Определить элемент списка [1, 2, 3, 4, 6, 7, 9], имеющий минимальное отклонение от среднего значения элементов списка. 2 Вывести все элементы дерева в порядке их возрастания. Вариант 11 1 Определить минимальный элемент списка чисел [5, 2, 9, 8, 3, 1]. 2 Определить число вхождений элемента 8 в дерево. Вариант 12 1 Найти предпоследний по величине элемент списка чисел [1, 5, 2, 6, 8]. 2 Вычислить сумму значений всех элементов дерева. Вариант 13 1 Сформировать новый список из тех элементов списка [7, 1, 3, 8, 5, 4, 2], которые стоят на позициях, кратных трем 2 Определить, входят ли элементы 1 и 4 в дерево. Вариант 14 42 1 Подсчитать количество целых чисел, входящих в список [6, 6, 5, 5, 4, 3, 2, 2, 1] только один раз. Список упорядочен по убыванию. 2 Найти наименьший элемент дерева. Вариант 15 1 Найти сумму элементов данного списка, которые стоят на нечетных позициях. Список [5, 1, 3, 7, 8, 4]. 2 Найти число вершин дерева, являющихся листьями Вариант 16 1. Определить, является ли список симметричным. 2. Определить, сколько элементов дерева находятся на 3м уровне. 43 Лабораторная работа №3 Тема: Среда программирования Visual Prolog. Цель: Знакомство со средой Visual Prolog, основанной на языке логического программирования ПРОЛОГ, изучение принципов построения экспертных систем в данной среде. Задание 1) В соответствии с вариантом задания на 1-ю лабораторную работу написать программы решения задач в среде Visual Prolog. 2) Разработать в среде Visual Prolog экспертную систему в соответствии с заданием. Теоретическая часть 1 Возможности Visual Рго1оg Возможности Пролога выходят далеко за границы области искусственного интеллекта. Высокий уровень абстракции, легкость и простота, с помощью которых представляются сложные структуры данных, позволяют применять декларативный подход к программированию, что приносит пользу любой стратегии решения задач. По этой причине Visual Рго1оg широко используется для создания административных приложений, управления сложными базами данных, систем календарного планирования, Web-приложений и многих других. Рассмотрим некоторые преимущества Visual Рго1оg. Визуальная среда разработки (VDE) Визуальная среда разработки (VDE, Visual Development Environment) объединяет компилятор с редактором, комплектом инструментальных средств разработки ресурсов, экспертами ресурсов и приложений, интерактивной утилитой построения приложений и различными средствами просмотра кода. После интерактивного визуального создания компонентов пользовательского интерфейса автоматически генерируется исполняемая программа. Эксперт приложений автоматически создает все необходимые файлы проекта, а эксперт ресурсов знает, как сгенерировать код Пролога, который будет обеспечивать использование всех выбранных ресурсов. Визуальная среда разработки (рис. 3.1) позволяет легко, удобно и быстро создавать приложения, основанные на высокоуровневой абстракции стандартных пользовательских интерфейсов, обеспечиваемых каждой из поддерживаемых операционных систем. Функционально идентичные среды визуальной разработки могут выполняться на всех платформах Windows. 44 Рис. 3.1 – Визуальная среда разработки (VDE) Эксперты кода Эксперты кода генерируют и поддерживают Пролог-код, управляющий использованием ресурсов. Одно из несомненных преимуществ Visual Рго1оg — комбинация инструментальных средств разработки ресурсов и экспертов кода, поддерживающих использование этих ресурсов для создания графического пользовательского интерфейса. Это означает, что вы можете создать новое приложение за несколько минут и затем последовательно расширять этот прототип до конечного приложения. Эксперт окон и диалоговых окон изображен на рис.3.2. Рис. 3.2 – Эксперт окон и диалоговых окон 45 Эксперт приложений Эксперт приложений генерирует и конфигурирует новые проекты. Он учитывает комбинации операционных систем, стратегии пользовательского интерфейса, особенности используемого компоновщика связей, сопутствующих инструментальных средств, используемых пакетов и т. д. После того как будет сгенерирован новый проект, эксперт приложений автоматически установит все базовые инструментальные средства, такие как файл справки, панели инструментов, меню и т. д. Окно эксперта приложений изображено на рис.3.3. Рис. 3.3 - Эксперт приложений Интегрированные редакторы для подготовки ресурсов Эти инструментальные средства позволяют визуально проектировать и изменять пользовательский интерфейс в интерактивном режиме. Элементы управления размещаются в диалоговых окнах при помощи мыши. Щелкнув на элементе кнопкой мыши, вы получите доступ к настройке параметров этого элемента. Ресурсы — это диалоговые окна, растровые изображения, пиктограммы, курсоры и строки. Они необходимы для любого приложения, использующего графический интерфейс пользователя (GUI, Graphical User Interface). Возможность импорта ресурсов Ресурсы могут быть импортированы из динамически связанных библиотек (DLL), приложений, файлов ресурсов и из других проектов Visual Рго1оg. На рис.3.4 приведено диалоговое окно импорта ресурсов. Рис.3.4 - Диалоговое окно импорта ресурсов 46 Текстовый редактор Visual Рго1оg обладает широким спектром возможностей, присущих любой современной среде разработки, например, мощный редактор исходного кода с цветным выделением ключевых слов и других элементов языка Visual Рго1оg. Цвета позволяют легко различать имена предикатов, параметры, комментарии и т. д., например, целочисленные константы могут отображаться красным цветом. Редактор поддерживает возможность неограниченной отмены и восстановления изменений, поиск и замену, операции вырезания, копирования и вставки, быстрое перемещение блоков кода при помощи мыши. Редактор позволяет также внедрять гипертекстовые ссылки. Более того, как и в предыдущих редакторах РОС, вы можете включать функции редактора в ваши собственные приложения. Уникальной особенностью редактора, используемого в визуальной среде разработки, является то, что он "знает" обо всех предикатах, компонентах пользовательского интерфейса, цветах, константах и т. д. Все эти элементы могут быть легко вставлены в исходный код щелчком мыши. Интегрированный создатель подсказок Встроенная система создания подсказок и помощи позволяет очень просто создавать интерактивную подсказку в вашем приложении. Система подсказок основана на гипертекстовой абстрактной машине. В системе подсказок можно вводить текст подсказки в интерактивном режиме, создавать новые связи и переходить по существующим связям во время фазы проектирования. Система подсказок может генерировать промежуточные файлы в формате RTF (стандартный формат для Windows-компиляторов помощи) для создания собственных справочных систем. Браузер исходного кода Компилятор Visual Рго1оg генерирует информацию для браузера исходного кода, что позволяет просматривать предикаты модуля, все глобальные предикаты проекта, находить определения и объявления предикатов и доменов, а также классов и фактов. Интерактивная справка Visual Рго1оg Эта справочная система предлагает полное руководство по операциям визуальной среды разработки, всю справочную информацию по основам языка Пролог и интерфейсу визуального программирования (VPI, Visual Programming Interface). Интерфейс визуального программирования (VРI) Для графических интерфейсов пользователя был определен переносимый интерфейс прикладного программирования (АРI, Application Programming Interface) Visual Рго1оg, который является абстракцией возможностей основных оконных операционных систем Windows 3.x, Windows 95/98/МЕ и Windows NT/2000. Этот интерфейс визуального программирования предоставляет программисту GUI АРI, который одновременно является и переносимым, и более 47 легким в использовании, чем программирование на "родных" GUI АРI. Однако, чтобы не ограничивать пользователей, VPI содержит и некоторые непереносимые средства и свойства. Если в программе используются непереносимые средства, то приложение также будет непереносимым. Сделать приложение переносимым поможет условная компиляция, которая обеспечит альтернативное поведение приложения ни различных платформах. Компоненты (пакеты) GUI высокого уровня Наряду с основным переносимым VPI, некоторые высокоуровневые компоненты GUI были выполнены с использованием VPI. Эти компоненты (пакеты) поставляются вместе с исходным кодом, и, конечно, переносимы на все платформы, поддерживаемые VPI. Эти инструментальные средства включают в себя таблицу, окно дерева, окно просмотра каталогов, панели инструментов, снабженные вкладками диалоговые окна, усовершенствованное средство генерации отчетов и т. д. Компилятор Быстрый высокооптимизированный компилятор Visual Рго1оg генерирует компактный код, который вполне может конкурировать с кодом, генерируемым компиляторами С и Раsсаl. Кроме генерации эффективного кода, компилятор выполняет множество дополнительных проверок для обнаружения возможных проблем во время компиляции. Основные проверки — это контроль соответствия типов, глобальный анализ потоков, проверка детерминизма и обнаружение возможных сбоев. Диалоговое окно Compiler Options (Опции компилятора) представлено на рис. 3.5. Рис. 3.5 – Диалоговое окно Compiler Options Отладчик При работе с большими программами на Прологе вам понадобится отладчик Visual Рго1оg. Отладчик работает с откомпилированным кодом. В исходном коде можно ставить точки останова и выполнять программу по шагам. В режиме пошагового выполнения программы можно просматривать значения переменных и содержимое утвержденных фактов. Окно отладчика представлено на рис. 3.6. 48 Рис. 3.6 – Окно отладчика Классы и объекты Часто различают объектно-ориентированные и декларативные языки программирования, но в Visual Prolog вы можете использовать возможности обеих парадигм. Язык Visual Prolog поддерживает объекты и классы так же, как C++. class aclass predicates testpred (string) check() store() endclass implement aclass facts f (aclass) clauses store ():this (ORef), assert (f(ORef)). heck():f(ORef), !, ORef: testpred("Check Virtual methods"). testpred (TXT):write (TXT), nl. endclass goal O = aclass::new (), O: store, O: check(). 49 Переносимый код Система Visual Prolog работает под разными операционными системами и может генерировать программы для различных платформ. Не считая нескольких специфических особенностей и ограничений операционных систем, код Пролога переносим на любую из них. Функции, подобные копированию, переименованию и удалению файлов, вызову других программ, получению значений даты и времени и т. п., работают одинаково на всех платформах. Приложения могут быть созданы для DOS, расширенного DOS, Windows 3.x, Windows 95/98/ME, Windows NT/2000 и Linux (VPI не поддерживается под Linux, DOS и расширенным DOS). Открытая платформа Visual Prolog способен взаимодействовать с другими программными средствами. Он может генерировать подпрограммы, вызываемые из других языков, а может и самостоятельно вызывать подпрограммы, написанные на других языках. Интерфейс является общим и поддерживает все компиляторы, которые генерируют стандартные модули OBJ. Программы Visual Prolog могут как вызывать DLL, так и сами могут быть помешены в DLL. Объявляя глобальные предикаты Visual Prolog, как имеющие соглашения о вызовах language С (или language stdcall), и домены, соответствующие С типам аргументов, и соответствующие потоки ввода/вывода, можно вызывать функции С непосредственно (как если бы они были написаны на Прологе) без какоголибо специального связующего кода между Visual Prolog и С. Этот интерфейс работает в обоих направлениях: когда предикаты Visual Prolog объявляются в language С, они могут вызываться прямо из программ, написанных на языке С. global predicates procedure long vpi_LoadDll (string) — (i) language с procedure vpi_FreeDll (long) — (i) language с procedure long vpi_GetDllProc(long, string ProcName) — (i,i) language с Подсистема баз данных Быстрая и чрезвычайно гибкая подсистема баз данных делает Visual Prolog весьма удобным средством для разработки приложений баз данных. Система баз данных поддерживает набор отдельных упорядоченных последовательностей термов Пролога. Термы баз данных могут быть любой абстракцией, поддерживаемой языком, от простых записей до деревьев или графов. Система баз данных может обращаться прямо к отдельным термам или совершать поиск с возвратом по последовательностям термов, чтобы генерировать или подбирать требуемые значения. Термы могут быть сохранены в файле или в памяти. Подсистема баз данных поддерживает также В+ деревья, которые обеспечивают быстрый поиск данных и возможность эффективного изменения порядка термов. При использовании подсистемы баз данных в сетевых приложениях Visual Prolog предоставляет возможность разделения файлов во внешней базе данных. Многопользовательский доступ к базе данных обеспечивается при помощи механизма одновременного доступа к файлам несколькими пользователями или процессами. 50 2 Программы Visual Prolog Синтаксис Visual Prolog разработан для того, чтобы отображать знания о свойствах и взаимосвязях. В отличие от других версий Пролога, Visual Prolog — компилятор, контролирующий типы: для каждого предиката объявляются типы объектов, которые он может использовать. Это объявление типов позволяет программам Visual Prolog быть скомпилированными непосредственно в машинные коды, при этом, скорость выполнения сравнима, а в некоторых случаях — и превышает скорости аналогичных программ на языках С и Pascal. Обычно программа на Visual Prolog состоит из четырех основных программных разделов. К ним относятся: раздел clauses (предложений); раздел predicates (предикатов); раздел domains (доменов); раздел goal (целей). Эти разделы были описаны в лабораторной работе №1. Рассмотрим другие, часто используемые разделы программ facts, constants и global разделы. Раздел фактов Программа на Visual Prolog представляет собой набор фактов и правил. Иногда в процессе работы программы бывает необходимо модифицировать (изменить, удалить или добавить) некоторые из фактов, с которыми она работает. В этом случае факты рассматриваются как динамическая или внутренняя база данных, которая при выполнении программы может изменяться. Для объявления фактов программы, рассматривающихся как части динамической (или изменяющейся) базы данных, Visual Prolog включает специальный раздел — facts. Ключевое слово facts объявляет раздел фактов. Именно в этой секции вы объявляете факты, включаемые в динамическую базу данных. Отметим, что в ранних версиях Visual Prolog для объявления раздела фактов использовалось ключевое слово database, т.е. ключевое слово facts — синоним устаревшего ключевого слова database. В Visual Prolog есть несколько встроенных предикатов, обретающих использование динамических фактов. Разделам facts можно давать имена (которые создают соответствующий внутренний домен). По умолчанию доменом для неименованного раздела facts будет домен bdasedom. В программе может присутствовать несколько разделов facts, но каждый из них при этом должен иметь уникальное имя. Предикат базы фактов можно описать только в одном разделе facts. С помощью стандартных предикатов asserta, assertz и consult можно добавлять факты к внутренней базе данных фактов во время работы программы. С помощью предикатов retract и retractall можно удалять эти факты во время работы программы. Предикат save сохраняет факты из внутренней базы фактов в файле (в определенном формате). С помощью редактора можно создавать или редактировать такой файл фактов, а затем можно вносить факты из файла в программу с помощью предиката consult. 51 Ваша программа может обращаться к предикатам базы фактов в программе таким же образом, как и ко всем другим предикатам. При использовании внутренних доменов, сгенерированных для имен разделов facts, можно работать с фактами как с термами. Раздел констант В своих программах на Visual Prolog вы можете объявлять и использовать символические константы. Раздел для объявления констант обозначается ключевым словом constants, за которым следуют сами объявления, использующие следующий синтаксис: <Id> = <Макроопределение> <Id> — имя символической константы, а <макроопределение> — это то, что вы присваиваете этой константе. Каждое <Макроопределение> завершается символом новой строки и, следовательно, на одной строке может быть только одно описание константы. Объявленные таким образом константы могут позже использоваться в программах. Рассмотрим следующий фрагмент программы: constants zero = 0 one = 1 two = 2 hundred = |10*(10-1)+10) pi = 3.141592653 ega = 3 slash_fill = 4 red = 4 Перед компиляцией программы Visual Prolog заменит каждую константу на соответствующую ей строку. Например, фрагмент программы: …, А = hundred*34, delay(A), setfillstyle(slash_fill, red), Circumf = pi*Diam, …, будет обрабатываться компилятором, как следующий фрагмент: …, А= (10*(10-1)+10)*34,delay(A), setfillstyle(4, 4), Circumf = 3.141592653*Diam, …, На использование символических констант накладываются следующие ограничения: описание константы не может ссылаться само на себя: my_number = 2*my_nurober/2 % не допускается это приведет к сообщению об ошибке "Recursion in constant definition" (Рекурсия в описании константы); в описаниях констант система не различает верхний и нижний регистры. Следовательно, при использовании в разделе программы clauses 52 идентификатора типа constants, его первая буква должна быть строчной для того, чтобы избежать путаницы между константами и переменными. Поэтому следующий фрагмент программы является допустимой конструкцией: constants Two = 2 goal A=two, write(A). в программе может быть несколько разделов constants, однако объявление константы должно производиться перед ее использованием; идентификаторы констант являются глобальными и могут объявляться только один раз. Множественное объявление одного и того же идентификатора приведет к сообщению об ошибке "Constant identifier can only be declared once" (Идентификатор константы может объявляться только один раз). Глобальные разделы Visual Prolog позволяет объявлять некоторые разделы domains, predicates, clauses глобальными (а не локальными); сделать это вы можете, объявив в своей программе специальные разделы global domains, global predicates и global facts. Директивы компилятора Visual Prolog поддерживает несколько директив компилятора, которые можно добавлять в программу для сообщения компилятору специальных инструкций по обработке вашей программы при ее компиляции. Кроме этого вы можете устанавливать большинство директив компилятора с помощью команды меню среды визуальной разработки Visual Prolog Options | Project | Compiler Options. Директива include Для того чтобы избежать многократного набора повторяющихся процедур, вы можете использовать директиву include. Ниже приведен пример того, как это делается: 1.Создаете файл (например, MYSTUFF.PRO),в котором объявляете свои наиболее часто используемые предикаты (с помощью разделов domains и predicates) и даете их описание в разделе сlauses. 2. Пишете исходный текст программы, которая будет использовать эти процедуры. 3. В "допустимых областях" исходного текста программы размещаете строку: include "mystuff.pro" ("Допустимые области" — это любое место программы, в котором вы можете расположить декларацию разделов domains, facts, predicates, clauses или goal). При компиляции исходных текстов программы Visual Prolog вставит содержание файла MYSTUFF PRO прямо в окончательный текст файла для компиляции. 53 Директиву include можно использовать для включения в исходный текст (практически любого) часто используемого фрагмента. Кроме того, любой включаемый в программу файл может, в свою очередь, включать другой файл (однако каждый файл может быть включен в вашу программу только один раз). Повторим: директива include может располагаться в любых "допустимых областях" программы. Основные положения, которые необходимо знать при написании Прологпрограмм: Пролог-программа состоит из предложений, которые могут быть фразами двух типов: (фактами или правилами) • факты — это связи или свойства, о которых вы (программист) твердо знаете, что они истинны; • правила — это зависимые связи (отношения); они позволяют Прологу выводить один фрагмент информации из другого. Факты имеют общий вид: property (objectl, object2,…, objectN) или relation (objectl, object2, …., objectN) где property — это свойство объектов, a relation — отношение между объектами. Различия между этими понятиями несущественны, и, поэтому, в дальнейшем мы будем использовать термин отношение. Каждый факт программы задает либо отношение, влияющее на один или более объектов, либо свойство одного или более объектов. Например, в факте Пролога likes (torn, baseball) отношение — это likes (нравится), а объекты — torn и baseball (бейсбол); Тому нравится бейсбол. В другом факте left_handed(benjamin) left_handed (левый крайний) является свойством объекта benjamin; т.е. Бенджамин- левый крайний Правила имеют общую форму Заголовок:- Тело, которые выглядят так: relation(object,object, … ,object):relation(object, … ,object), … … relation(object, … ,object). Сообразуясь со следующими ограничениями, вы можете устанавливать любые имена для связей и объектов • имя объекта должно начинаться со строчной буквы, за которой может быть любое число символов. Этими символами могут быть буквы верхнего и нижнего регистров, цифры и символы подчеркивания; 54 • имена свойств и связей должны начинаться со строчной буквы, за которой может следовать любая комбинация букв, цифр и символов подчеркивания. Предикат — это символическое имя (идентификатор) связи с последовательностью аргументов. Программа на Прологе — это последовательность предложении и директив, а процедура — это последовательность предложении, описывающих предикат. Предложения, принадлежащие одному предикату, должны следовать друг за другом. Переменные позволяют вам записывать общие факты и правила и задавать общие вопросы: • имя переменной в Visual Prolog должно начинаться с заглавной буквы или символа подчеркивания (_), после которой вы можете использовать любое число букв (верхнего и нижнего регистра), цифр и символов подчеркивания; • переменные в Прологе получают свои значения в результате сопоставления с константами в фактах или правилах. До получения значения переменная является свободной, после — становится связанной; • вы не можете длительно хранить информацию с помощью "связывания" переменной со значением, т.к. переменная является связанной только в пределах предложения. Если в запросе вас интересует только определенная информация, то для игнорирования не нужных вам значений вы можете использовать анонимные переменные В Прологе анонимные переменные обозначаются одиночным символом подчеркивания (_}. Анонимная переменная может быть использована вместо любой другой переменной, она сопоставляется с любыми данными. Анонимная переменная никогда не принимает какого-либо значения. Задание вопросов о фактах в программе называется запросами к системе. Пролога; более общим термином для запроса является цель (goal). Пролог пытается разрешить цель (ответить на вопрос), просматривает все факты, начиная с первого до достижения последнего из них. Составная цель — это цель, включающая две или более частей, каждая часть составной цели называется подцелью. Составная цель может быть конъюнктивной (подцель А и подцель В) или дизъюнктивной (подцель А или подцель В). Комментарии делают вашу программу более удобной для чтения. Вы можете заключать комментарии в разделители или предварять их одним символом процента. В Прологе имеется несколько способов сопоставления одного объекта с другим: • идентичные структуры сопоставляются друг с другом; • свободная переменная сопоставляется с константой или с ранее связанной переменной (и становится связанной с соответствующим значением); • две свободные переменные могут сопоставляться (и связываться) друг с другом. С момента связывания они трактуются как одна переменная если одна из них принимает какое-либо значение, то вторая немедленно принимает то же значение. 55 Программа на Visual Prolog имеет следующую обобщенную структуру: Domains объявления доменов */… predicates /* … объявления предикатов */… clauses /*… предложения (правила и факты) */ goal /*… подцель __1, подцель _2, и т.д. */ В разделе clauses вы размешаете факты и правила, с которыми будет работать Visuaд Prolog, пытаясь разрешить цель программы. В разделе predicates вы объявляете предикаты и домены (типы) аргументов этих предикатов. Имена предикатов должны начинаться с буквы (желательно строчной), за которой следует последовательность букв, цифр и символов подчеркивания (до 250 знаков). В именах предикатов нельзя использовать символы пробел, минус, звездочка, слэш. Объявление предиката имеет следующую форму: predicates predicateName(argumentTypel OptionalNamel, argumentType2 OptionalName2, < … >, argumentTypeN OptionalNameN) Здесь argument_typel, …, argument_typeN — либо стандартные домены, либо домены, объявленные в разделе domains. Объявление домена аргумента и описание типа аргумента — суть одно и то же. Имена аргументов OptionalNamel будут игнорироваться компилятором. В разделе domains объявляются любые нестандартные домены, используемые вами для аргументов предикатов. Домены в Прологе являются аналогами типов в других языках. Основные стандартные домены Visual Prolog: char, byte, short, ushort, word, integer, unsigned, long, along, dword, real, string и symbol. Основная форма объявления доменов имеет вид: domains myDomainl, … , myDomainN = <standardDomain> Форма объявления составных доменов имеет следующий вид: myDomainl,...,myDomainN=<compoundDomain_l>; <compoundDomain_2>; 56 <… >; <compoundDomain_M> В данной главе не рассматривались составные домены; о них подробнее см гл 12. В разделе goal вы задаете внутреннюю цель программы, это позволяет программе быть скомпилированной, запускаться и выполняться независимо от среды визуальной разработки (VDE). Арность (размерность) предиката — это число принимаемых им аргументов; два предиката с одним именем могут иметь различную арность. Предикаты с отличающимися арностями должны собираться вместе, причем и в разделе predicates, и в разделе clauses, но такие предикаты рассматриваются как абсолютно разные. Правила имеют форму НЕAD: - <Subgoal1>, <Subgoal2>, … , <SubgoalN>. Для разрешения правила Пролог должен разрешить все его подцели, создав при этом соответствующее множество связанных переменных. Если же одна из подцелей ложна, Пролог возвратится назад и просмотрит альтернативные решения предыдущих подцелей, а затем вновь пойдет вперед, но с другими значениями переменных. Этот процесс называется поиск с возвратом. Оператор Пролога: - (if) отличается от IF, используемых в других языках: правило Пролога работает в соответствии с условной формой тогда/если, тогда как этот оператор в других языках работает в соответствии с условной формой если/тогда. 3 Визуальное программирование на Visual Prolog Даже если вы не знакомы с синтаксисом Visual Prolog, это не помешает вам работать с приведенными далее примерами, но вам нужно научиться пользоваться системой интерактивной справки, если вы не сделали этого ранее. В особенности вам необходимо уметь находить информацию по предикатам VPI (Visual Programming Interface, интерфейс визуального программирования). Итак, запускаем среду визуальной разработки Visual Prolog (VDE, Visual Development Environment). Начало работы с экспертом приложений Эксперт приложений (Application Expert) используется для создания нового приложения. Его можно вызвать при помощи пункта меню Project | New Project. Оставьте настройки по умолчанию, а именно: платформа Windows32 в списке Platform, “VPI” – в списке UI Strategy, “exe” – в списке Target Type и “Prolog” – в списке Main Program. Необходимо заполнить поле Project Name и выбрать/определить основной каталог в поле Base Directory для проекта. Если вы задаете несуществующий основной каталог, эксперт приложений создаст его. Возможно, вы не захотите поместить проект в каталог, предложенный экспертом приложений по умолчанию. Чтобы выбрать другой (существующий) каталог, нажмите кнопку Browse, которая вызывает диалоговое окно с деревом каталогов, где вы можете просматривать локальные (или сетевые) диски для 57 поиска подходящего места для нового проекта; здесь же можно напечатать название необходимого подкаталога (тогда вы непосредственно сможете редактировать содержимое поля Base Directory). Для нашего примера мы выберем каталог C:\TEMP, куда в его собственном подкаталоге TST будет помещен проект. Отметьте флажок Tree Package, т. к. мы будем использовать этот инструмент в нашем демонстрационном приложении. Заданные по умолчанию панели инструментов для проекта создаются только тогда, когда создан новый проект командой Project | New Project. Если флажок Toolbar and Help Line не был отмечен во время создания проекта, можно активизировать эксперт приложений в существующем проекте (при помощи комбинации клавиш <Ctrl> + <A>) и включить опцию Toolbar and Help Line. Это даст возможность использовать в проекте панель инструментов, но при этом код и расположение панели инструментов придется генерировать «вручную» при помощи редактора панели инструментов (Toolbar Editor) и эксперта панели инструментов (Toolbar Expert). После установки свойств VPI можно нажать кнопку Create для генерации всех необходимых файлов и создания приложения по умолчанию. Кнопка Create расположена в нижней части диалогового окна Application Expert. В окне среды визуальной разработки сейчас должны быть два исходных модуля: VPITools.pro и Myрroj.pro. Использование команды Project | Run После того как проект создан, команда меню Project | Run откомпилирует и выполнит его. Запустите проект командой меню Projact | Run, или используя «горячую» клавишу <F9>, или нажав кнопку на панели инструментов. На этой стадии выполняемые функции доступны только в некоторых пунктах меню. Проект по умолчанию – это основа для формирования вашего собственного приложения. В меню Help уже есть пункт About, а в дальнейшем будет сгенерирован и небольшой справочный файл. Кроме того, ваше приложение будет иметь панель инструментов, строку подсказки и окно Messages, которое вы можете использовать для отладки и тестирования результатов. Рис. 3.7 - Окно проекта 58 Сгенерированный исходный код Вы видите, что для нашего проекта были сгенерированы два файла: Myproj.pro – основной модуль и VPITools.pro – модуль, который включает в себя инструментальные средства, выбранные в эксперте приложений. Чтобы посмотреть структуру кода основного модуля проекта, выделите файл Myproj.pro и нажмите кнопку Edit. Следует обратить внимание, что код сгенерирован для следующих элементов: обработка событий для окна Task; основная цель (goal) проекта; создание в проекте панели инструментов и строки подсказки; создание окна About; отображение заданной по умолчанию справки. Дерево проекта Дерево проекта обеспечивает обзор файлов, используемых в проекте. Оно может быть активизировано при помощи команды меню Project | Tree, комбинацией клавиш <Ctrl>+<T> или кнопки панели инструментов. Двойной щелчок мыши на имени текстового файла внутри дерева проекта вызовет текстовый редактор для этого файла. Браузер исходного кода Попробуйте активизировать браузер исходного кода (Source Code Browser) (рис.8), используя пункт меню Project | Browse. Рис.3.8 - Окно браузера кода Это простой способ найти: объявления и реализации классов; объявления доменов; объявления и предложения предикатов и фактов, используемых в проекте. Браузер кода также можно вызвать нажатием кнопки панели инструментов или при помощи комбинации клавиш <Ctrl>+<B>. 59 Приложение «Hello World» Попытаемся создать небольшое диалоговое окно, отображающее «Hello World» (Привет, Мир!). Допустим, мы хотим добавить пункт меню, который, когда он выбран, будет активизировать предопределенное общее диалоговое окно dlg_Note. Для этого необходимо сделать следующее: 1. Добавить пункт меню. 2. Добавить предложение, чтобы определить действия, выполняемые при выборе пункта меню (обработчик событий в меню Task Menu для захвата события). 3. В этом предложении сделать вызов предиката dlg_Note (наша реакция на выбор пункта меню). Изменение меню в редакторе меню В окне проекта нажмите кнопку Menu на левой панели инструментов. Когда в окне проекта появится список меню, зарегистрированных в проекте, дважды щелкните мышью на пункте Task Menu для активизации редактора меню. Рис.3.9 - Добавление пункта меню в редакторе меню Выберите пункт меню Edit и добавьте новый пункт меню верхнего уровня с именем Test нажатием кнопки New. Вам не нужно вводить имя – константу для пункта меню: он автоматически получит константу id_test по умолчанию. Кстати, символ & который вы видите в именах некоторых пунктов меню, определяет, что при отображении меню символ, следующий за &, отображается подчеркнутым. Это используется для определения «горячих» клавиш. «Горячую» клавишу можно определить, напечатать символ в поле Accelerator и выбрать соответствующую комбинацию клавиш <Shift>, <Alt> или <Ctrl>. 60 Далее нажмите кнопку Submenu для создания подменю с одним пунктом Hello World, который мы будем использовать, чтобы активизировать приглашение Hello World (рис. 3.10). Рис.3.10 - Редактирование подменю в редакторе меню Позднее вы подключите пункт меню к выполняемому коду, используя значения константы, которое мы сейчас определим. Нажмите кнопку Attributes или просто дважды щелкните на пункте меню Hello World. Это активизирует диалоговое окно Menu Item Attributes для пункта меню Hello World (рис.11). Замените автоматически регенерированную константу id_Test_hello_world на id_hello. Нажмите кнопку OK, чтобы подтвердить сделанное изменение. Рис.3.11 - Редактирование атрибутов меню В диалоговом окне Menu Item Attributes вы можете определить, будет ли пункт меню неактивным (закрашен серым цветом) или отмечен палочкой. Если необходимо здесь можно определить «горячую» клавишу. Теперь можно протестировать ваше меню. Нажмите кнопку Back, пока она не станет не активной. Затем вы можете выбрать режим Test для предварительного просмотра меню. В режиме Test тестируемое меню появляется вместо меню задачи среды визуальной разработки, можно открыть и просмотреть его подменю. Чтобы выйти из режима Test, нужно щелкнуть мышью в любом окне среды визуальной разработки за пределами меню или снова нажать кнопку Test. Закройте редактор меню при помощи кнопки Close. Использование эксперта окон и диалоговых окон Теперь можно воспользоваться текстовым редактором VPI для того, чтобы создать диалоговое окно Hello World. Однако в Visual Prolog проще и 61 правильнее использовать эксперты кода. Эксперты кода могут выполнять часть стандартной работы, и, что более важно, они помогут в организации кода. Кроме того, они позволяют легко ориентироваться в коде. Рекомендуем всегда использовать эксперта окон и диалоговых окон (Dialog and Window Expert) при добавлении исходного кода Пролог для пунктов меню. Попробуй сделать это сейчас. Сейчас нажми кнопку Window в окне проекта; будет отображен список имен, зарегистрированных в проекте окон. Выберите окно с именем Task Window, это окно пока единственное зарегистрированное в проекте. Теперь нажмите кнопку Code Expert на правой стороне окна проекта (или используйте комбинацию клавиш <Ctrl>+<W> для вызова эксперта окон и диалоговых окон). После того как появиться эксперт окон и деловых окон, вам следует выполнить следующие шаги: 1. Выбрать пункт Menu в списке Event Type слева. 2. Выделить строку td hello или любую другую константу, которую вы в качестве идентификатора ресурса связали с пунктом меню Hello World в списке Event or Item. Возможно, придется использовать полосу прокрутки для поиска. 3. Нажать кнопку Add Clause, сгенерировать Пролог-предложение для события. Название кнопки Add Clause изменяются на Edit Clause, когда код для события будет создан. 4. Нажать кнопку Edit Clause. Нажатие кнопки Edit Clause вызовет редактор (для файла Myproj. Pro). Он позиционируется на соответствующем предложении обработчика событий. Обратите внимание, что к предикату обработчика событий для окна Task было добавлено следующее предложение: %BEGIN Task Window, id_hello task_win_eh(_Win,e_Menu(id_hello,_ShiftCtlAlt),0):-!, !. %END Task Window, id_hello Комментарии %Begin – %End анализируется экспертом окон и диалоговых окон, чтобы определять положение кода для компонентов пользовательского интерфейса. Переменная _Win содержит дескриптор окна, в данном случае это дескриптор окна Task. Переменная _ShiftCtrlAlt содержит флаг, из которого вы можете получить комбинацию клавиш <Shift>, <Alt> и <Ctrl>, которая использовалась, когда меню было активизировано. В обоих случаях символ подчеркивания – это синтаксическое соглашение, которое отменяет предупреждение компилятора для однократного использования имен переменных (что обычно бывает с неиспользованными переменными). Добавить код можно с помощью меню Insert редактора. Попробуйте поместить курсор редактора в точке, где вы хотите вставить код (в данном случае, в начале строки после первого отсечения (!) в предложении), затем щелкните правой кнопкой мыши и наберите команду Insert | Predicate Call | Window, Dialog or Toolbar. 62 В появившемся сейчас диалоговом окне используйте кнопку раскрытия списка для выбора общего диалогового окна dlg_Note и напечатайте «Hello World» (включая кавычки) (рис.3.12). Рис.3.12 - Определение предиката вставки Заданный по умолчанию запрос к указанному предикату (вызов общего диалогового окна dlg_Note с определенными параметрами) будет затем вставлен в текст таким образом, чтобы окончательный код предложения выглядел так: task_win_eh(_Win,e_Menu(id_hello,_ShiftCtlAlt),0):-!, Title="Title", dlg_Note(Title,"Hello World"), !. Последнее отсечение в действительности не является необходимым и будет автоматически удалено компилятором Пролога, но это облегчает возможность вставить код при помощи меню вставки. Теперь выберите пункт меню Project | Run (или нажмите кнопку ) для того, чтобы приложение выполнилось. При выборе команды Test | Helo World появится диалоговое окно, представленное на рис.3.13. Рис3.13 - Диалоговое окно, которое появится при выполнении приложения 4 Пример построения экспертной системы в среде Visual Prolog Воспользуемся Visual Prolog для построения небольшой экспертной системы, которая будет угадывать одно из семи животных (если такое существует), задуманное пользователем. Система будет задавать вопросы и строить логические выводы на основе полученных ответов. Этот пример наглядно демонстрирует поиск с возвратом, использование базы данных и эффективное применение предиката not. В качестве примера можно привести типичный диалог экспертной системы с пользователем (для большего понимания дан перевод): has it hair? (Оно имеет шерсть?) yes (да) 63 does it eat meat? (Оно ест мясо?) yes (да) has it a fawn color? (Оно желтовато-коричневой масти?) yes (да) has it dark spots? (Оно имеет темные пятна?) yes (да) Your animal may be a cheetah! (Ваше животное должно быть гепард!) Именно способность Visual Prolog проверять факты и правила обеспечивает программе свойства, присущие экспертной системе. Первым шагом при построении такой системы является обеспечение ее знаниями, необходимыми для выполнения рассуждений; это, напомним, носит название механизма логического вывода. Механизм логического вывода рассмотрен на примере нижеприведенной программы. global facts xpositive(symbol,symbol) xnegative(symbol,symbol) predicates nondeterm animal_is(symbol) nondeterm it_is(symbol) ask(symbol,symbol,symbol) remember(symbol,symbol,symbol) positive(symbol,symbol) negative(symbol,symbol) clear_facts run clauses animal_is(cheetah):it_is (mammal), it_is(carnivore), positive(has,tawny_color), positive{has,dark_spots). animal_is(tiger):it_is (mammal), it_is(carnivore), positive(has, tawny_color), positive(has, black_stripes). animal_is(giraffe):it_is(ungulate), positive(has,long_neck), positive(has,long_legs), positive(has, dark_spots). animal_is(zebra):-it_is(ungulate), positive(has,black_stripes). animal_is(ostrich):it_is(bird), negative(does,fly), positive(has,long_neck), 64 positive(has,long_legs), positive(has, black_and_white_color). animal_is(penguin):it_is(bird), negative(does,fly), positive(does,swim), positive(bas,black_and_white_color). aniraal_is(albatross):it_is(bird),positive(does,fly_well). it_is(mammal):positive(has,hair). it_is(mammal):positive(does,give_milk). it_is(bird):positive(has,feathers). it_is(bird):positive(does,fly), positive(does,lay_eggs). it_is(carnivore):positive{does,eat_meat). it_is(carnivore):positive(has,pointed_teeth), positive(has, claws), positive(has,forward_eyes). it_is(ungulate):it_is (mammal) , positive(has,hooves). it_is(ungulate):it_is(mammal), positive(does,chew_cud). positive(X,Y):xpositive(X,Y) , !. positive(X,Y):not(xnegative(X,Y)), ask(X,Y,yes). negative(X,Y):xnegative(X,Y),!. negative(X,Y):not(xpositive(X,Y)), ask(X,Y,no). ask(X,Y,yes) :!, write(X," it ",Y,'\n'), readln(Reply),nl, 65 frontchar(Reply, 'y',_), remember(X,Y,yes). ask(X,У,no):write(X," it ",Y,'\n'), readln(Reply),nl, frontchar(Reply,'n',_), remember(X,Y,no). remember(X,Y,yes):assertz(xpositive(X,Y)). remember(X,Y,no):assertz(xnegative(X,Y)). clear_facts:write("\n\nPlease press the space bar to exit\n"), retractall (_, dbasedom), readchar (_) . run:animal_is(X),!, write("\nYour animal may be a (an) ",X), nl,nl,clear_facts. run :write("\nUnable to determine what"), write("your animal is.\n\n"), clear_facts. goal run. Каждое животное описывается рядом признаков, которыми оно обладает (или нет). Ответы на вопросы пользователя описываются через предикаты positive (X,Y) и negative (X,Y). Следовательно, система может задать, например, такой вопрос: Does it have hair? (У него есть шерсть?) Получив ответ на этот вопрос, программа должна иметь возможность сохранить его в базе данных так, чтобы впоследствии использовать этот ответ в своих рассуждениях. Для простоты в пример были включены только положительные и отрицательные ответы. Для их использования в базу данных включены два предиката: facts xpositive(symbol, symbol) xnegative(symbol, symbol) Факт, что животное не имеет шерсти, будет записан так: xnegative(has, hair). Правила positive и negative используются для контроля ответов пользователя и задания новых вопросов. positive(X,Y) :xpositive(X,Y), !. positive(X,Y) :not(xnegative(X,Y)), ask(X,Y,yes). negative(X,Y) :66 xnegative(X,Y), !. negative(X,Y):not(xpositive(X,Y)), ask(X,Y,no). Заметьте, что второе правило как для positive, так и для negative гарантирует, что при задании вопроса пользователю не возникнет противоречие. Предикат ask служит для "формулирования" вопросов, он же "запоминает" ответы. Если ответ начинается с буквы у, то система предполагает, что ответом является yes (да), а если с n — то по (нет). Проанализируем листинг: Задание вопросов и анализ ответов ask(X, Y, yes) :- !, write(X, " it ", Y, '\n'), readln(Reply), frontchar(Reply, 'y', _), remember(X, Y, yes). ask(X, Y, no) :- !, write(X, " it ", Y, '\n'), readln(Reply), frontchar (Reply, 'n', __}, remember(X, Y, no). 4 remember(X, Y, yes) :- assertz{xpositive(X, Y)). remember(X, Y, no) :assertz(xnegative(X, Y)). /* Уничтожение всех старых фактов */ clear_facts :- write("\n\nPlease press the space bar to exit\n"), retractall(_,dbasedom), readchar(_). Для практики введите в компьютер приведенный выше механизм логического вывода и базу знаний. Прибавьте к ним ряд объявлений для дополнения программы и затем проверьте, какой будет получен результат. Полный текст программы с экспертной системой по животным был приведен ранее. Варианты заданий 1. ЭС диагностики заболеваний 2. ЭС «Абитуриент» 3. ЭС «Авиа-диспетчер» 4. ЭС «Кредитные операции» 5. ЭС «Поиск неисправностей ПК» 6. ЭС «Прогноз погоды» 7. ЭС «Выбор автомобиля» 8. ЭС «Психология» 9. ЭС «Тарифы мобильной связи» 10. ЭС «Подбор конфигурации ПК» 67 Лабораторная работа №4 Тема: Среда программирования Actor Prolog. Цель: Знакомство со средой Actor Prolog, основанной на языке логического программирования ПРОЛОГ. Задание В соответствии с вариантом задания на 3-ю лабораторную работу разработать экспертную систему в среде Actor Prolog. Теоретическая часть Акторный Пролог объектно-ориентированный логический язык, предназначенный для программирования информационных систем, функционирующих в динамическом внешнем окружении (интеллектуальных агентов Интернет, систем интерактивного проектирования и др.). В Акторном Прологе информационная система представляется в виде теоремы на логическом языке, разделённой на "логические акторы" - повторно доказываемые подцели, взаимодействующие через общие переменные. Доказательство логических акторов (далее - просто "акторов") осуществляется в объектноориентированном пространстве поиска, топология которого соответствует структуре системы. Акторный Пролог воплощает новый подход к объединению логического и объектно-ориентированного программирования, обладающий следующими достоинствами: В основе подхода лежит использование классической логики (логики предикатов первого порядка). Центральной идеей и сущностью подхода является обнаружение и устранение логических противоречий, возникающих в процессе взаимодействия объектов. Разработанный подход позволил математически корректным образом ввести в логический язык разрушающее присваивание и параллельные процессы. В Акторном Прологе внешние воздействия, вызывающие отклик информационной системы, интерпретируются как использование разрушающего присваивания, вызывающего повторное доказательство акторов логической программы. В частности, взаимодействие человека и машины рассматривается как доказательство некоторой теоремы, в котором одновременно принимают участие человек, изменяющий исходные данные, и машина, обеспечивающая корректность и полноту доказательства. Стратегия управления Акторного Пролога - акторный механизм - является расширением стандартной стратегии управления языка Пролог (стратегии "поиска слева направо в глубину с возвратом"). В отличие от стандартной стратегии управления, акторный механизм допускает разрушающее присваивание значений общим переменным и при этом автоматически поддерживает корректность доказательства теоремы с помощью повторного доказательства отдельных акторов. 68 1. АЛФАВИТ ЯЗЫКА В качестве алфавита языка используется набор символов ASCII, при этом различаются графические символы (графемы), имеющие визуальное представление в виде отпечатанного знака или пробела, и управляющие символы: возврат на одну позицию, горизонтальная табуляция, перевод строки, вертикальная табуляция, перевод формата и возврат каретки. Минимальный набор графических символов, достаточный для определения языка, включает буквы, цифры, символ пробела и специальные символы: ! " # ' ( ) * + , - . / : ; < = > ? [ \ ] _ ` { | }. 2. ЛЕКСИКА Текст программы рассматривается как последовательность лексем и разделителей. Разделителями являются комментарии, а также пробелы и управляющие символы, не входящие в состав лексем и комментариев. Чтобы обеспечить однозначность трансляции текста, приняты следующие соглашения: Сканирование текста всегда осуществляется слева направо. В состав каждой лексемы включается по возможности большее число графических символов. Фрагмент текста ":-" не является лексемой, если он расположен между лексемами "{" и "}", составляющими пару "открывающая скобка - закрывающая скобка". Фрагмент текста "<-" не является лексемой, если он расположен непосредственно перед числовым литералом или ограничителем "(". Пример. Последовательность лексем и разделителей. Текст "P{a:-7}:-P{/*/b:0}.--1--" содержит лексемы "P", "{", "a", ":", "-", "7", "}", ":", "P", "{", "b", ":", "0", "}", "." и комментарии "/*/", "--1--". 2.1. ЛЕКСЕМЫ Лексемами являются: переменные, символы и ключевые слова, целые числовые литералы, вещественные числовые литералы, сегменты строк, ограничители. В ходе сканирования текста происходит преобразование информации, поэтому в определении языка различаются собственно "лексемы", воспринимаемые лексическим анализатором, и "значения лексем", которые обрабатывает синтаксический анализатор. 2.1.1. ПЕРЕМЕННЫЕ Переменная - это имя, начинающееся с большой буквы или символа подчёркивания "_". Маленькие буквы в составе переменной заменяются соответствующими большими буквами, при этом все остальные графемы остаются без изменений. Полученная последовательность графем считается значением лексемы. Переменная "_" называется "анонимной". Считается, что все анонимные переменные являются некоторыми уникальными, однократно использованными именами. Пример. Правильно построенные переменные: A1, _, AbC_Ef_H7, _7, Variable, _X_123 69 2.1.2. СИМВОЛЫ И КЛЮЧЕВЫЕ СЛОВА Символ - это имя, начинающееся с маленькой буквы или заключённое в апострофы. Различаются простые символы и символы в апострофах: Большие буквы в составе символа заменяются соответствующими маленькими буквами, при этом все остальные графемы остаются без изменений. Полученная последовательность графем считается значением символа. Апострофы, в которые может быть заключён символ, не являются составными частями его значения. Если апострофы не используются, значение символа не должно совпадать с ключевыми словами языка. Ключевыми словами являются следующие имена: as - под_именем class - класс import - импортировать from - из package - пакет project - проект protecting - защищающий specializing - специализирующий suspending - отключающий Для написания ключевых слов языка используются только маленькие буквы. Значениями ключевых слов считаются соответствующие цепочки графем. Пример. Правильно построенные символы: symbol, 'ALPHA', abc_EF_h, '', s4734 2.1.3. ЧИСЛОВЫЕ ЛИТЕРАЛЫ Числовой литерал - это лексема, обозначающая числовое значение. Числовые литералы бывают целые и вещественные (плавающие) - значениями таких литералов являются, соответственно, (беззнаковые) целые и вещественные числа. По умолчанию основание числового литерала равно 10. Основание и порядок числовых литералов всегда записываются в десятичной системе. В качестве (расширенных) цифр от 10 до 35 используются латинские буквы от "A" до "Z" (от "a" до "z") соответственно. Значение каждой (расширенной) цифры литерала с основанием должно быть меньше основания. Числовые литералы, содержащие точку, обозначают вещественные числа. Использование пробела, так же как и управляющих символов в определении числового литерала не допускается (считается синтаксической ошибкой). Символы подчёркивания между соседними цифрами и буквами числового литерала не влияют на его значение. Для получения значения числового литерала с порядком необходимо умножить значение числового литерала без порядка на основание, возведённое в указанную порядком степень. Порядок целых числовых литералов не может содержать знак минус. Пример. Правильно построенные числовые литералы: 13_274, 2#1100_0100#E4, 39.123e100, 8#177_777#, 3.217_514e+90, 16#EF93#, `y, 8#3.51#E-31 70 2.1.4. СЕГМЕНТЫ СТРОК Сегмент строки - это лексема, обозначающая цепочку графических и управляющих символов. В ходе сканирования сегмента строки конструкции вида "\"код (где код некоторая буква или числовой литерал) заменяются соответствующими графическими и управляющими символами. код = "b" | "t" | "n" | "v" | "f" | "r" | числовой_литерал Буквенные коды соответствуют управляющим символам: b - возврат на одну позицию; t - горизонтальная табуляция; n - перевод строки; v - вертикальная табуляция; f - перевод формата; r - возврат каретки. В качестве кода в сегменте строки не допускается использование вещественных числовых литералов, а также числовых литералов, значения которых лежат за пределами некоторого интервала, определяемого конкретной реализацией языка. В случае если графический символ, следующий после "\", не является кодом, переключатель "\" игнорируется, а обнаруженный за ним графический символ включается в сегмент строки без дальнейшего анализа. Полученная таким образом последовательность графических и управляющих символов, не считая кавычек, в которые заключён сегмент строки, является значением сегмента строки. Пример. Правильно построенные сегменты строк: "String \"XYZ\"\n", "", "c:\\dos\\*.*" 2.1.5. ОГРАНИЧИТЕЛИ Ограничитель - это последовательность из одного или нескольких специальных символов, используемая в синтаксических конструкциях языка. В языке используются: простые ограничители !#()*+,-./:;<=>?[]{|} составные ограничители :- << <- ?? == := <> <= >= Значениями ограничителей считаются соответствующие цепочки графем. 2.2. КОММЕНТАРИИ Комментарием является последовательность графических и управляющих символов, начинающаяся с открывающей скобки комментария и заканчивающаяся закрывающей скобкой. Комментариям разных типов соответствуют разные скобки. Открывающая скобка не является началом комментария, если её графические символы входят в состав лексемы или другого комментария. Определены два типа комментариев: Однострочный комментарий: открывающая скобка - два соседних дефиса; закрывающая - любой управляющий символ, отличный от горизонтальной табуляции. Многострочный комментарий: открывающая и закрывающая скобки "/*" и "*/" соответственно. Повторное вхождение открывающей скобки "/*" в состав многострочного комментария считается синтаксической ошибкой. Пример. Правильно построенные комментарии: -- Однострочный комментарий 71 /*/ /* Многострочные комментарии */ /*/ 3. ОПРЕДЕЛЕНИЕ ДАННЫХ В общем случае, термы языка могут обозначать: элементы данных; экземпляры классов; значения лексем "переменная" (если речь идёт о несвязанных переменных). терм = простой_терм | составной_терм | вызов_функции_в_предложении Элементы данных создаются в ходе исполнения вызовов предикатов, во время построения слотов миров, а также во время глобальных операций с общими переменными. В дальнейшем, когда будет идти речь об унификации и других операциях с термами, следует иметь в виду обработку значений термов. В качестве функторов составных термов и атомарных формул используются символы и метапеременные (метафункторы): функтор = символ | метапеременная Метапеременными называются переменные, используемые в качестве функторов и символов. Метапеременные, используемые в качестве функторов, называются метафункторами. метапеременная = переменная В качестве функторов метапеременные разрешается использовать только в составе предложений и только при условии, что такой же метафунктор является именем предиката в заголовке рассматриваемого предложения. Функтор, используемый в составе определения класса и совпадающий с некоторым атрибутом этого класса, должен быть символом в апострофах. 3.1. ПРОСТЫЕ ТЕРМЫ Простой терм - это элементарная синтаксическая конструкция, обозначающая данные и миры. Простыми термами являются константы (символ, целое число, вещественное число, строковый литерал, спейсер #, метапеременная, обозначающая терм в метапредложении), а также параметры. Число обозначается с помощью числового литерала, перед которым может стоять знак минус. В целых и вещественных числах с явно указанным основанием использовать знак минус не разрешается. Строковый литерал - это последовательность сегментов строки, обозначающая цепочку графических и управляющих символов: строковый_литерал = [ строковый_литерал ] сегмент_строки Спейсер # обозначает неизвестный элемент данных или мир. Метапеременные разрешается использовать в качестве простых термов только в составе предложений и только при условии, что такая же метапеременная является именем предиката или атомарной формулой в заголовке рассматриваемого предложения. 72 параметр = переменная | атрибут Считается, что значением терма "переменная" является значение лексемы "переменная", до тех пор, пока эта переменная (терм) не будет связана с какойлибо константой, составным термом или миром. Значением связанной переменной считается соответствующий элемент данных, мир или спейсер. Переменная, не связанная с константой, составным термом или миром, называется "несвязанной". Значения других простых термов определяются значениями соответствующих им лексем. Диапазоны допустимых целых и вещественных чисел определяются конкретной реализацией языка. При этом значения числовых литералов с явно указанным основанием (выходящие за пределы допустимого диапазона) разрешается использовать в качестве битового представления отрицательных чисел. Значением строкового литерала является конкатенация значений последовательности входящих в его состав сегментов строк. Максимальная допустимая длина значения строкового литерала определяется конкретной реализацией языка. Пример. Правильно построенные простые термы: VARIABLE, 'symbol', 2#0100_1100#, -34.0e-9, "A" "TEXT" "LINE\n" 3.2. СОСТАВНЫЕ ТЕРМЫ Составными термами являются структуры, списки и недоопределённые множества. Значения составных термов определяются с помощью кортежей и некоторых специальных имён (констант). 3.2.1. СТРУКТУРЫ Структура - это составной терм, построенный из функтора и последовательности одного или более аргументов, заключённой в круглые скобки: структура = функтор "(" термы_и_выражения ")" термы_и_выражения = [ термы_и_выражения "," ] терм_или_выражение терм_или_выражение = терм | выражение Значением структуры f(A1,A2,...,An) является кортеж длины n+2, в первой позиции которого стоит специальная константа structure: <structure,f,A1,A2,...,An>. Пример. Правильно построенные структуры: g1(1+2,X,Y), functor(i(1-(R*12),2,3),4,k(5),Z), h(J) 3.2.2. СПИСКИ Список - это составной терм, построенный из последовательности (возможно, пустой) аргументов, заключённой в квадратные скобки. В случае если последовательность аргументов списка не является пустой, в его состав может быть включён дополнительный компонент, обозначающий остаток (хвост) списка: список = "[" [ термы_и_выражения [ "|" хвост ] ] "]" хвост = параметр | вызов_функции_в_предложении | выражение 73 Значением пустого списка [] является специальная константа #empty_list. Значением списка [A1,A2,...,An|Rest] является кортеж <list,A1,<list,A2,...<list,An,Rest>...>>, где list - специальная константа, Rest - хвост списка. Таким образом, терму [A1,A2,...,An] соответствует значение <list,A1,<list,A2,...<list,An,#empty_list>...>>. Пример. Правильно построенные списки: [17,_,"item_of_list",321,93,_], [X+721,Y,R+H,Z|R], [] 3.2.3. НЕДООПРЕДЕЛЁННЫЕ МНОЖЕСТВА Недоопределённое множество - это составной терм, построенный из набора (возможно, пустого) элементов, заключённого в фигурные скобки. Элементы недоопределённого множества задаются в виде пар "имя_элемента: значение_элемента", где имя элемента - некоторый символ или неотрицательное целое число, а значение элемента - терм или выражение: элементы_множества = [ элементы_множества "," ] элемент_множества элемент_множества = имя_элемента [ ":" терм_или_выражение ] | атрибут имя_элемента = символ | числовой_литерал Если в составе элемента множества не заданы терм или выражение после имени элемента, значением такого элемента считается анонимная переменная "_". В таких случаях символы, используемые в качестве имён элементов, обязательно должны быть в апострофах. Если недоопределённое множество используется в составе определения класса, то имена элементов множества, совпадающие с атрибутами этого класса, должны быть символами в апострофах. Если в качестве элемента множества задан атрибут Name, то именем элемента множества считается символ 'Name', а значением - слот Name. Заголовком недоопределённого множества называется значение элемента с именем 0 (ноль), которое может быть задано в начале недоопределённого множества, за пределами фигурных скобок. При таком способе определения в качестве заголовка недоопределённого множества разрешается использовать только простые термы: Недоопределённое множество вида F{x1:A1,x2:A2,...,xn:An|Rest}, в составе которого задан заголовок F, эквивалентно {0:F,x1:A1,x2:A2,...,xn:An|Rest}. В случае если набор элементов множества (учитывая заголовок) не является пустым, в составе множества может быть задан дополнительный компонент, обозначающий неопределённый остаток (хвост) множества. Если недоопределённое множество используется в составе определения атрибутов класса, в качестве хвоста этого множества разрешается использовать только переменные. 74 недоопределённое_множество = [ простой_терм ] "{" элементы_и_хвост_множества "}" элементы_и_хвост_множества = [ элементы_множества ] [ "|" хвост ] Недоопределённое множество не может содержать пары с одинаковыми именами элементов. Для того чтобы построить значение недоопределённого множества, необходимо: Просмотреть полный текст программы и построить множество S всех имён элементов всех недоопределённых множеств, которые в ней используются. С помощью лексикографического упорядочения из элементов множества S построить цепочку s1,s2,...,sm (m - мощность множества S). Если недоопределённое множество обозначает конечное число элементов (случай {sX:AX,sY:AY,...,sZ:AZ}), его значением является кортеж длины m+1: <set,...,z,...,t(AY),...,z,...,t(AX),...,t(AZ),...>, где set - специальная константа, t - имя вспомогательного функтора. Каждая позиция i+1 (i=1,...,m) кортежа содержит значение t(Ai), если пара с именем элемента si присутствует в рассматриваемом терме, или, если такая пара не обнаружена, специальную константу z. Значением пустого множества {}, в частности, является кортеж вида <set,z,z,z,z,...z,z,z>. Значением недоопределённого множества общего вида {sX:AX,sY:AY,...,sZ:AZ|Rest} является кортеж длины m+1: G1 = <set,...,V1,...,t(AY),...,V2,...,t(AX),...>, каждая позиция i+1 (i=1,...,m) которого содержит t(Ai), если пара с именем элемента si присутствует в рассматриваемом терме, или некоторую уникальную переменную Vj, если соответствующая пара не обнаружена. Кроме того, понадобится ещё один кортеж G2 = <set,...,V1,...,z,...,V2,...,z,...>, который отличается от предыдущего тем, что все аргументы t(A) в нём заменены константами z. Считается, что всякий раз, когда создаётся значение недоопределённого множества G1, одновременно с этим происходит унификация G2 с переменной Rest. Пример. Правильно построенные недоопределённые множества: R2{x:17,y:-(1+X),'z',5:'yes'}, {q:Y,e:W,symbol:_,k:0|W}, {} 3.3. УНИФИКАЦИЯ ТЕРМОВ Унификацией называется операция сравнения (отождествления) нескольких формул, связывающая переменные в составе формул сопоставленными с ними подформулами. Унификация термов осуществляется в ходе исполнения вызовов предикатов, в момент создания значений недоопределённых множеств, а также во время глобальных операций с общими переменными. Кроме того, унификация термов может быть вызвана явно с помощью встроенного предиката "унифицировать термы": L == R. 75 Встроенный предикат '==' разрешается использовать с произвольным количеством аргументов: '=='(V1,...,Vk). Унификация несвязанной переменной с константой, составным термом или миром вызывает "связывание" этой переменной - замену всех вхождений этой переменной соответствующим элементом данных или миром. Унификация различных несвязанных переменных вызывает их "сцепление" (отождествление): в дальнейшем любое связывание одной из сцеплённых переменных автоматически вызывает такое же связывание всех сцеплённых с ней переменных. В Акторном Прологе область действия операций связывания и сцепления переменной всегда ограничена множеством её вхождений, принадлежащих некоторым конкретным акторам. В Акторном Прологе допускается унификация целых и вещественных чисел. Унификация целого и вещественного чисел заканчивается успехом тогда и только тогда, когда вещественное число обозначает целую величину, равную унифицируемому с ней целому числу. Константы и составные термы несопоставимы между собой (их унификация невозможна). Невозможна также унификация констант и составных термов с экземплярами классов. Унификация термов, обозначающих миры, возможна лишь в том случае, если они обозначают один и тот же мир. Спейсер не может быть унифицирован ни с какой другой константой, кроме себя. Проверкой вхождения называется специальная операция, осуществляемая в ходе унификации, предотвращающая (запрещающая) связывание переменной с составными термами, содержащими эту переменную. В соответствии с семантикой Акторного Пролога, проверка вхождения не распространяется на переменные в составе миров, являющихся компонентами унифицируемых термов. Пример. Унификация двух составных термов: {region:X,name:"Baikal"|Rest1} == {name:Y,object:'lake',region:"Siberia"} В ходе унификации элементы одного множества будут унифицированы с элементами другого в соответствии с заданными именами элементов. Результатом унификации станут подстановки X="Siberia", Y="Baikal", на месте переменной Rest1 окажется значение G недоопределённого множества, включающего однуединственную пару object:'lake'. Остальные позиции кортежа G (кроме первой, которая всегда содержит set) будут заполнены z. 4. СТРУКТУРА ПРОГРАММЫ Программа состоит из множества классов и целевого утверждения ("проекта"): программа = { определение_класса | определение_проекта } Будем говорить, что некоторый класс C (или проект) "использует" класс E, если E является предком C в иерархии наследования классов, а также если C (проект) или кто-либо из его предков содержит конструктор экземпляра класса E (или конструктор экземпляра класса F, такого что класс F использует класс E), не 76 считая тех конструкторов, которые входят в состав инициализаторов, перекрываемых во время построения соответствующих миров. В программе должны быть определены все классы, используемые проектом. Исполнением программы называется построение и дальнейшее согласование некоторых процессов. Исполнение программы начинается с доказательства конструктора процесса, заданного в определении проекта, а также формирования процесса, построенного в результате доказательства этого конструктора. 4.1. КЛАССЫ Класс - это набор предложений языка, имеющий уникальное имя и входящий в состав иерархии наследования: определение_класса = class заголовок_класса ":" атрибуты "[" предложения "]" В языке используется одиночное наследование: у класса может быть не более одного непосредственного предка и неограниченное число потомков. Имя непосредственного предка указывается в определении после имени класса: заголовок_класса = имя_класса [ specializing имя_класса ] имя_класса = символ_в_апострофах В иерархии наследования классов, используемых проектом, запрещены циклические зависимости. Примечание. Неаккуратное (взаимно-) рекурсивное использование классов может приводить к бесконечному увеличению количества миров в ходе формирования экземпляров классов. Пример. Правильно построенный класс. class 'MyWindow' specializing 'Report': text_color = 'Green'; [ goal:show,!. ] 4.1.1. АТРИБУТЫ КЛАССОВ Атрибутами называются имена слотов экземпляра класса, определяемые в составе класса. Каждый атрибут должен быть объявлен во всех классах, связанных отношением наследования, в которых используется соответствующий слот. Область действия атрибута распространяется на инициализаторы слотов в определении атрибутов класса, а также на все предложения класса. атрибуты = { определение_атрибута ";" } определение_атрибута = [ описатель_порта ":" ] атрибут [ "=" инициализатор ] описатель_порта = suspending | protecting атрибут = простой_символ В составе инициализаторов слотов могут использоваться переменные. Область действия таких переменных ограничена множеством инициализаторов слотов в 77 определении атрибутов класса. В определении атрибутов класса не допускается однократное использование переменных, отличных от "_". Атрибут self - предопределённый, он обозначает непосредственно тот экземпляр класса, в котором это имя используется. Повторное определение атрибутов класса (в том числе переопределение атрибута self) считается синтаксической ошибкой. Пример. Правильно определённые атрибуты класса: a = Y; e = ('Q',x='+'(a,f),m=self,k=e); b; f = '*'(Y,7); c = 'f'(_,[3,7],Y,a); d = []; g = _; h = {x:1,y:Y,z:R|_}; i = [0,_,j|R]; j = a; 4.1.2. ИНИЦИАЛИЗАТОРЫ СЛОТОВ Инициализатором слота называется синтаксическая конструкция, определяющая начальное значение слота: инициализатор = терм | конструктор 4.1.3. КОНСТРУКТОРЫ Конструктором называется утверждение о существовании экземпляра класса или резидента. В результате доказательства конструкторов происходит построение новых экземпляров классов и резидентов. Различаются конструкторы миров (а именно простые конструкторы и конструкторы процессов), а также конструкторы резидентов. конструктор = конструктор_мира | конструктор_резидента конструктор_мира = простой_конструктор | конструктор_процесса Простым конструктором называется элементарное логическое утверждение о существовании экземпляра класса. простой_конструктор = "(" имя_класса { "," определение_атрибута } ")" Конструктор процесса - это утверждение о существовании процесса. Доказательство конструктора процесса приводит к созданию нового процесса. конструктор_процесса = "(" простой_конструктор ")" Аргументы конструктора мира определяют значения слотов соответствующего экземпляра класса (значения слотов процесса). Отсутствие инициализатора в определении некоторого атрибута конструктора с именем Name является допустимым только в том случае, если рассматриваемый конструктор экземпляра класса находится в области действия слота с именем Name. Такое определение атрибута эквивалентно определению вида "Name=Name". В конструкторе экземпляра класса не допускается определение нескольких атрибутов с одинаковыми именами. Не допускается также определение атрибута self. Конструктором резидента называется синтаксическая конструкция, определяющая резидента. Конструктор резидента определяет целевые миры 78 резидента и соответствующую резиденту атомарную формулу. Доказательство конструктора резидента приводит к созданию нового резидента. конструктор_резидента = [ параметр_или_конструктор ] "??" простой_атом параметр_или_конструктор = целевой_параметр | конструктор_мира целевой_параметр = параметр Атомарная формула в составе конструктора резидента является вызовом функции. В качестве простых атомов в конструкторе резидента не разрешается использовать метаатомы. Пример. Правильно построенные конструкторы: ('R53',a=1,b=7,c=_,'d'), (('W',q=3)), ('E') ?? f(A) 4.2. ПРОЕКТ Проектом называется целевое утверждение программы - некоторый конструктор процесса: определение_проекта = project ":" конструктор_процесса В составе проекта могут использоваться переменные. Область действия таких переменных ограничена пределами проекта. В определении проекта не допускается однократное использование переменных, отличных от "_". В качестве обозначения экземпляра класса, соответствующего проекту, в определении проекта допускается использование предопределённого атрибута self. Проект доказывается по правилам исполнения конструкторов процессов. Пример. Правильно построенный проект. project: (('P',x=[1,7,9|W],y=W,p=self)) 4.3. ПАКЕТЫ Пакетом называется совокупность классов, связанных между собой по смыслу. Текст программы может состоять из нескольких пакетов. Каждый пакет должен храниться в отдельном исходном файле. Каждый исходный файл программы считается отдельным пакетом. Каждый пакет обладает собственной областью видимости имён классов. Таким образом, имена классов, используемые внутри пакета, не видны из других пакетов до тех пор, пока не будут импортированы в эти пакеты явным образом, с помощью команд импорта. Целевое утверждение (проект) программы может входить в состав любого пакета. В общем случае, каждый пакет включает заголовок пакета, команды импорта, а также произвольное количество классов и (возможно) целевое утверждение (проект): пакет = [ заголовок_пакета ] команды_импорта программа заголовок_пакета = package имя_пакета ":" имя_пакета = строковый_литерал В начале исходного файла может быть указан заголовок пакета, определяющий имя пакета. Именем пакета является строковый литерал, представляющий собой 79 имя соответствующего исходного файла без расширения. В качестве имени пакета допускается использование: Полного имени файла, включающего все подкаталоги. Укороченного имени файла, если пакет расположен в одном из подкаталогов системного каталога, который должен быть определён в конкретной реализации языка. В этом случае имя пакета должно включать лишь подкаталоги, вложенные по отношению к системному каталогу. В качестве разделителя имён подкаталогов в имени пакета допускается использование как прямой "/", так и обратной "\\" дробной черты (реализация языка должна одинаково интерпретировать оба варианта). Имя пакета должно соответствовать реальному имени исходного файла. Несоответствие имени транслируемого пакета, не позволяющее однозначно сопоставить его с именем исходного файла, является синтаксической ошибкой. Если заголовок пакета не задан, имя пакета определяется автоматически по имени соответствующего исходного файла. Команда импорта делает видимым в пакете имя класса (заданное после ключевого слова import), определённого в некотором другом пакете (имя которого указано после ключевого слова from). Класс, заданный в команде импорта, называется импортируемым классом. Действия, осуществляемые командой импорта называются импортом класса. команды_импорта = { команда_импорта } команда_импорта = import импортируемое_имя from имя_пакета ";" импортируемое_имя = имя_класса [ as имя_класса ] Если в составе команды импорта задано ключевое слово as, импортируемый класс будет видим в текущем пакете под именем, заданным после ключевого слова as. Если ключевое слово as не задано, импортируемый класс будет видим под своим собственным именем. Последовательность команд импорта, которая делает видимыми некоторые импортируемые классы под одним и тем же именем, является синтаксической ошибкой. Использование в пакете нескольких команд импорта, которые делают видимым один и тот же класс из одного и того же пакета, является синтаксической ошибкой. Пример. Правильно построенные команды импорта: import 'Sphere' as 'Marker' from "VRML/Shapes"; import 'Analyzer' from "My\\Features"; 4.4. ТРАНСЛЯЦИЯ ИСХОДНЫХ ФАЙЛОВ Исходные файлы программы могут транслироваться отдельно друг от друга или совместно (конкретная реализация языка может поддерживать один из указанных способов или оба способа трансляции). В конкретной реализации языка может осуществляться автоматическая трансляция пакетов, указанных в командах импорта в составе других пакетов. В зависимости от реализации языка, автоматическая трансляция пакетов может рассматриваться как трансляция этих пакетов отдельно от других пакетов или как совместная трансляция этих пакетов. 80 Определения классов и определение проекта являются "элементарными программными модулями", из которых строится исходный файл. Результат их трансляции - добавление или замена соответствующих "библиотечных модулей" в некоторой программной библиотеке, структура которой должна быть определена в конкретной реализации языка. Набор совместно транслируемых пакетов, так же как каждый отдельный пакет не могут содержать повторные определения классов. Допускается только одно определение проекта в каждом отдельном пакете и в наборе совместно транслируемых пакетов. Максимальная допустимая длина исходного файла определяется конкретной реализацией языка. Формированием программы называется сборка программы из библиотечных модулей, сопровождаемая проверкой её синтаксической правильности. Формирование программы может осуществляться перед началом или во время её исполнения, однако синтаксически правильной считается только такая программа, которая может быть сформирована перед началом исполнения. Примечание. Некоторые синтаксические ошибки обнаруживаются лишь на этапе формирования программы из библиотечных модулей. Такими ошибками являются отсутствие определения проекта или некоторых определений классов в программной библиотеке, а также циклы в иерархии наследования. Пример. Исходный файл программы. package "Examples/Example1": class 'Hello' -- Определение класса specializing 'Report': width = 25; -- Атрибуты height = 10; [ -- Предложения goal:write("Hello world !"). ] project: -- Определение проекта (('Hello', x=1, y=2, width=30, height=7)) 5. СТРУКТУРА ПРОСТРАНСТВА ПОИСКА Составными частями пространства поиска служат экземпляры классов. В общем случае, пространство поиска развёртывается динамически, в ходе исполнения программы. 5.1. ЭКЗЕМПЛЯРЫ КЛАССОВ Экземпляр класса ("мир") - это конкретное применение класса. В состав экземпляра класса входят: Предложения класса, а также предложения его предков. Слоты экземпляра класса. Слот - это составная часть экземпляра класса, характеризуемая именем и значением. Именем слота является некоторый атрибут, а значением слота - терм. 81 Построение экземпляров классов происходит в результате доказательства утверждений об их существовании - конструкторов. Мир B называется вложенным по отношению к миру A, если конструктор мира B является инициализатором некоторого слота мира A или какого-либо мира E, вложенного по отношению к A. Пример. Наследование предложений класса. Экземпляр класса 'CHERRY' содержит предложения colour и taste, определённые в классах 'CHERRY' и 'FRUIT'. class 'FRUIT': [ taste('sweet'). taste('sour'). ] class 'CHERRY' specializing 'FRUIT': [ colour('red'). ] 5.2. ПРОЦЕССЫ Процессом называется экземпляр класса, предложения которого исполняются параллельно по отношению к предложениям других процессов. Процессам соответствуют отдельные части пространства поиска, не пересекающиеся с другими процессами. Построение процессов осуществляется в результате доказательства конструкторов процессов. Создателем процесса называется процесс, одному из слотов миров которого соответствовал инициализатор - конструктор рассматриваемого процесса. Считается, что некоторый актор "принадлежит" процессу G, если этот актор доказывается, доказан или должен быть доказан в мире, входящем в состав процесса G. Исполнением процесса называется доказательство акторов, принадлежащих этому процессу. "Фазами" исполнения процесса называются законченные периоды исполнения процесса, соответствующие: обработке процессом сообщений; изменению состояния процесса. После (успешного) окончания очередной фазы исполнения процесса осуществляется "фиксирование" процесса, а именно: Устраняются все точки выбора, возникшие в течение этой фазы. Фиксируются все общие переменные всех акторов, принадлежащих процессу. Процесс, находящийся на очередной фазе своего исполнения, называется активным. Считается, что некоторые процессы "согласованы" между собой, если: Все они находятся в состояниях "доказан" и "неиспользуемый". Не требуется обработка потоковых и прямых сообщений процессами, находящимися в состоянии "доказан". 82 Не требуется обработка потоковых сообщений процессами, находящимися в состоянии "неиспользуемый". Производные значения общих переменных всех процессов могут быть унифицированы. Примечание. В языке используется только асинхронное взаимодействие между процессами, поэтому предикаты каждого процесса обладают декларативной семантикой, не зависящей от других процессов. 5.2.1. СОСТОЯНИЯ ПРОЦЕССА В каждый конкретный момент времени процесс находится в одном из трёх состояний: "объявленный"; "используемый"; "неиспользуемый". В состоянии "объявленный" процесс находится сразу после его создания. Объявленный процесс характеризуется тем, что соответствующие ему экземпляры классов ещё не сформированы. Пока процесс находится в состоянии "объявленный", обработка любых сообщений этим процессом откладывается. Считается, что объявленный процесс не имеет никаких производных значений, и что его акторы несогласованы. После формирования процесса он переходит в состояние "сформированный". "Используемый процесс" - это обобщающее название для следующих трёх состояний процесса: "сформированный"; "доказанный"; "неудачный". Состояние "неиспользуемый" характеризуется тем, что на некоторые отключающие порты процесса поданы задерживающие значения. Пока процесс находится в состоянии "неиспользуемый", обработка любых сообщений этим процессом откладывается. Считается, что неиспользуемый процесс не имеет никаких производных значений, и все его акторы согласованы. Переход процесса в состояние "неиспользуемый" называется "отключением" процесса. Переход процесса в состояние "используемый" называется "подключением" процесса. Переключение между состояниями процесса "используемый" и "неиспользуемый" происходит автоматически при получении им определённых разновидностей потоковых сообщений. При переходе из состояния "неиспользуемый" в состояние "используемый", процесс всегда оказывается в том конкретном состоянии, в котором он находился до перехода в состояние "неиспользуемый". Если до перехода в состояние "неиспользуемый" процесс находился в состоянии "объявленный", он автоматически переводится в состояние "сформированный". Переключение между различными разновидностями состояния "используемый" происходит в зависимости от результатов очередной фазы исполнения соответствующего процесса: Состояние процесса "сформированный" характеризуется тем, что некоторые акторы процесса ещё ни разу не были доказаны и, следовательно, не согласованы. 83 В состоянии "сформированный" процесс может обрабатывать переключающие сообщения, однако обработка любых информационных сообщений откладывается. Фаза исполнения процесса, перед началом которой он находился в состоянии "сформированный", называется инициализацией процесса. После завершения фазы инициализации процесс может перейти в состояние "доказанный" или остаться в состоянии "сформированный". Состояние "доказанный" ("доказан") характеризуется тем, что все акторы, принадлежащие процессу, согласованы. В этом состоянии процесс может обрабатывать как переключающие, так и информационные сообщения. После завершения фазы обработки сообщения процесс переходит в состояние "доказанный" или "неудачный". Состояние "неудачный" характеризуется тем, что акторы процесса выведены из согласованного состояния. В этом состоянии процесс может обрабатывать переключающие сообщения, а обработка информационных сообщений откладывается. После завершения очередной фазы обработки сообщения процесс может перейти в состояние "доказанный" или остаться в состоянии "неудачный". Переход процесса в состояние "неудачный" называется "нейтрализацией" процесса. 5.2.2. ПОРТЫ ПРОЦЕССОВ Переменные процесса G, которые могут принадлежать акторам других процессов, называются портами процесса G. В ходе исполнения программы каждому порту процесса ставятся в соответствие: Сорт порта: простой, отключающий или защищающий. Состояние порта - вспомогательное логическое значение: "согласованный" или "несогласованный". Актор-представитель порта процесса - некоторый вспомогательный актор, принадлежащий процессу. Текущее значение порта - некоторый вспомогательный терм. Сорт текущего значения порта - вспомогательное логическое значение: "защищённое" или "незащищённое". Производитель текущего значения порта - процесс, построивший текущее значение порта. Акторы-представители портов процесса предназначены для хранения информации, приходящей в процесс через порты в виде потоковых сообщений. В ходе обработки потокового сообщения, пришедшего в процесс через некоторый порт S, в начале соответствующей фазы исполнения процесса, осуществляется "активизация" порта S: (единственное) локальное значение актора-представителя порта S устанавливается равным значению порта S на момент начала рассматриваемой фазы исполнения процесса, после чего актор-представитель этого порта объявляется активным (считается, что доказательство этого актора успешно завершено). Акторы-представители, не активизированные в начале фазы исполнения процесса, используются в ходе исполнения процесса наравне с другими акторами процесса. При этом, однако, в случае нейтрализации акторов-представителей, повторное 84 доказательство этих акторов не осуществляется, и они остаются нейтральными до очередной активизации соответствующих портов. Порты процесса создаются (определяются) в ходе формирования процесса. Процесс относит каждый из своих портов к одному из трёх сортов: простой; отключающий; защищающий. Отключающими портами процессов называется разновидность портов, обладающая следующими свойствами: При получении через отключающий порт задерживающего значения процесс автоматически переводится в состояние "неиспользуемый". Когда значения всех отключающих портов процесса перестают быть задерживающими, он автоматически возвращается в состояние "используемый" (см. правила перехода процесса из состояния "неиспользуемый" в состояние "используемый" в разделе 5.2.1). Отключающий порт R процесса G всегда активизируется в начале фазы исполнения процесса G, если производителем текущего значения порта R является процесс, отличный от G. Защищающими портами процессов называется разновидность портов, обладающая следующими свойствами: Все потоковые сообщения, передаваемые процессом через защищающий порт автоматически объявляются защищёнными. Значения всех незащищённых сообщений, принимаемых процессом через защищающий порт игнорируются в ходе обработки этих сообщений (активизация порта не осуществляется). Если порт не является отключающим и не является защищающим, он называется (является) простым. Сорта портов задаются с помощью описателей портов или по умолчанию. Описателями портов служат ключевые слова "suspending" и "protecting", обозначающие "отключающий" и "защищающий" соответственно. Если некоторому порту процесса не поставлено в соответствие никаких описателей, этот порт является простым. Если в тексте программы некоторому порту процесса поставлены в соответствие оба описателя "suspending" и "protecting", порт является отключающим. 5.3. РЕЗИДЕНТЫ Резидентом называется специальная активная сущность, отслеживающая состояния некоторых (целевых) процессов и передающая собранную информацию своему владельцу. Резиденты создаются в результате доказательства конструкторов резидентов. В общем случае, каждому резиденту соответствуют: Процесс, являющийся "владельцем" резидента. Атомарная формула (вызов функции), заданная в конструкторе резидента. Некоторые "целевые" миры резидента. Процессы, в состав которых входят целевые миры резидента, называются "целевыми" процессами резидента. Некоторые переменные, являющиеся общими для резидента и его владельца. 85 Владельцем (создателем) резидента является процесс, одному из слотов миров которого соответствовал инициализатор - конструктор рассматриваемого резидента. Резидент взаимодействует со своим владельцем как некоторый процесс - с помощью переключающих потоковых сообщений, по правилам передачи потоковых сообщений, указанным в разделе 7.4.3. Целевыми мирами резидента считаются все экземпляры классов, входящие в состав значения целевого параметра в текущий момент времени - набор целевых миров может изменяться в ходе исполнения программы. Если вместо целевого параметра в конструкторе резидента задан конструктор мира, целевым параметром считается экземпляр класса, построенный в результате доказательства этого конструктора мира. Если целевой параметр или заменяющий его конструктор мира в конструкторе резидента не заданы, целевым параметром считается предопределённый атрибут self. Резидент решает следующие задачи и осуществляет следующие действия: Построение множества списков, соответствующих различным целевым мирам резидента. Каждый такой список должен содержать все значения, возвращаемые в результате исполнения вызова функции (заданного в составе конструктора резидента) в рассматриваемом целевом мире. Передача построенного множества списков владельцу резидента в составе (переключающего) потокового сообщения через защищающий порт резидента. В качестве потокового сообщения передаётся текущее значение целевого параметра в конструкторе резидента, в котором все целевые экземпляры классов заменены соответствующими им списками значений функции. В случае если некоторые списки ещё не построены, или если некоторые целевые процессы находятся в состоянии "неиспользуемый", "неудачный", "объявленный" или "сформированный", а также если в составе значения целевого параметра вместо некоторых миров присутствуют несвязанные переменные, вместо соответствующих списков значений функции в составе потокового сообщения передаются спейсеры #. Постоянное слежение за состояниями целевых процессов. Повторное построение и передача списков значений функции после каждой фазы исполнения целевого процесса. Приём новых значений через простые порты при изменении соответствующих общих переменных. Повторное построение и передача списков значений функции при получении новых значений через простые порты. Вычисление значений функции в целевом мире допускается лишь в том случае, если целевой процесс уже обработал все полученные им потоковые сообщения, и, следовательно, не имеет несогласованных портов. Перед началом вычисления значений функции осуществляется активизация некоторых портов целевого процесса в соответствии с правилами обработки прямых сообщений (см. раздел 7.4.2). Для вычисления значений функции в целевом мире (временно) создаётся и доказывается с откатом актор Q. В случае если доказательство актора Q завершается исключительной ситуацией, (недостроенный) список значений функции теряется, и резидент осуществляет повторную попытку построить список значений функции. 86 Перед отправлением построенного списка значений функции осуществляется его упорядочение и сокращение с помощью удаления повторных элементов. В конкретной реализации языка должны быть заданы однозначные правила упорядочения термов (значений функций). Примечание. Повторное построение списков значений функции резидента разрешается не производить в тех случаях, когда повторное исполнение функции не приведёт к получению новых значений. 5.4. ПОСТРОЕНИЕ ПРОСТРАНСТВА ПОИСКА Построение пространства поиска осуществляется в результате доказательства конструкторов экземпляров классов и реализуется посредством построения новых экземпляров классов. Последовательность действий, создающих пространство поиска и слоты отдельного экземпляра класса, называется "формированием" экземпляра класса. Построение экземпляра класса (создание нового экземпляра класса) включает следующие этапы: Формирование экземпляра класса. Доказательство предикатов goal() во всех сформированных на первом этапе мирах. Этапы формирования экземпляра класса и доказательства предикатов goal не зависят друг от друга и относятся к разным этапам построения процесса. Выполнение этих операций осуществляется в соответствии со следующими правилами: При построении экземпляров классов используются копии определений классов, отличающиеся от соответствующих определений классов тем, что все переменные в составе определений атрибутов этих классов заменяются новыми уникальными именами. Каждая автоматически исполняемая подцель goal объявляется актором. Исполнение предикатов goal осуществляется в произвольном порядке. При этом, однако, при равных прочих условиях, предикат goal в мире B всегда доказывается раньше, чем в мире A, если мир B является вложенным по отношению к миру A. 5.4.1. ИСПОЛНЕНИЕ КОНСТРУКТОРОВ Доказательство (исполнение) простого конструктора приводит к формированию нового экземпляра заданного класса C и включает следующие действия: Построение пространства поиска экземпляра класса C. В состав пространства поиска включаются предложения самого класса C, затем (в соответствии с иерархией наследования) предложения его непосредственного предка D, предложения непосредственного предка класса D и так далее, пока не будет достигнут класс, не имеющий предка в иерархии наследования. Построение слотов экземпляра класса. Доказательство (исполнение) конструктора процесса приводит к построению нового процесса. В результате доказательства конструктора процесса, новый процесс устанавливается в состояние "объявленный". Исполнение конструктора процесса не приводит к построению пространства поиска созданного процесса. Построение соответствующего пространства поиска и слотов процесса осуществляется позже, в ходе "формирования" процесса. 87 Формирование процесса вызывается процессом, который является его создателем (см. раздел 7.4.3). Формирование процесса включает следующие действия: Доказывается простой конструктор, заданный в составе конструктора процесса. Определяются порты процесса, в соответствии с описателями портов, заданными в аргументах конструктора процесса и определениях соответствующих классов. Проверяются текущие значения портов процесса и, в соответствии с правилами переключения состояний процесса "используемый" и "неиспользуемый" (см. раздел 5.2.1), процесс переводится в состояние "используемый сформированный" или в состояние "неиспользуемый". Процессу автоматически посылается инициализирующее потоковое сообщение. В случае если формирование процесса заканчивается аварийной ситуацией, вызывается встроенный обработчик ошибок. Доказательство (исполнение) конструктора резидента приводит к построению нового резидента. Доказательство конструктора резидента включает следующие действия: Переменная, созданная в качестве начального значения слота процесса-владельца, инициализатором которого является конструктор резидента (см. раздел 5.4.2), объявляется защищающим портом резидента. Все остальные общие переменные, заданные в составе конструктора резидента, объявляются простыми портами резидента. Новый резидент начинает функционировать. 5.4.2. ПОСТРОЕНИЕ СЛОТОВ Одновременно с созданием каждого слота, если для него задан соответствующий инициализатор, создаётся его "начальное" значение: Значение терма, если инициализатором слота является терм или конструктор резидента. Некоторый мир, если инициализатором является конструктор мира. Значение другого слота, если инициализатором является атрибут. В качестве инициализатора каждого создаваемого слота используется значение соответствующего аргумента доказываемого конструктора или, если аргумент не задан, инициализатор в определении класса. При этом инициализаторы, заданные в определении любого класса C, отменяют ("перекрывают") все инициализаторы соответствующих атрибутов в определениях предков класса C. Построение значения слота, инициализатором которого является конструктор, вызывает доказательство этого конструктора. Если инициализатором слота является конструктор резидента, создаётся специальная переменная, общая для резидента и его владельца. Эта переменная становится начальным значением слота процесса-владельца, инициализатором которого является названный конструктор резидента. Если инициализатором слота является конструктор резидента, в составе которого задан некоторый конструктор мира, указанный конструктор мира также доказывается. Слоты, не имеющие инициализаторов, а также слоты, (взаимно-) рекурсивно заданные в качестве своих собственных инициализаторов, получают в качестве начальных значений уникальные общие переменные. 88 Все переменные, создаваемые в составе значений слотов, также являются общими. Если в составе аргумента доказываемого конструктора или в определении соответствующего класса некоторому атрибуту приписан описатель порта, действие описателя распространяется на все общие переменные в составе инициализатора соответствующего слота, исключая конструкторы и вызовы функций, входящие в этот инициализатор. Описатели портов, заданные в составе конструктора, а также в определении соответствующих классов, перекрывают друг друга аналогично тому, как осуществляется перекрытие инициализаторов слотов. Перекрытие описателей портов происходит независимо от перекрытия инициализаторов слотов. Пример. Перекрытие инициализаторов слотов и описателей портов. Рассмотрим определение некоторых классов 'C', 'D' и 'E': class 'E': suspending: a = 21; protecting: b = 25; [ goal. ] class 'D' specializing 'E': protecting: a = 7; [] class 'C' specializing 'D': b = 8; [] В результате доказательства простого конструктора ('C', a=X) будет построен некоторый мир, значения слотов которого a=X, b=8. Переменная X будет объявлена защищающим портом. 6. ПРЕДЛОЖЕНИЯ КЛАССОВ Логические правила ("предложения") состоят из заголовка и последовательности (возможно, пустой) подцелей. Предложения, в составе которых нет подцелей, называются "фактами". предложение = атом [ ":-" конъюнкция ] "." конъюнкция = [ конъюнкция "," ] подцель В предложении могут использоваться переменные и атрибуты. Область действия переменных ограничена пределами предложения. В составе предложения не допускается однократное использование переменных, отличных от "_". Предложения, в которых используются метапеременные, называются метапредложениями. Предложения каждого класса группируются в соответствии с их заголовками. предложения = { предложение } Предложения, не являющиеся метапредложениями, должны принадлежать одной группе ("процедуре"), если совпадают имена и арность предикатных символов заголовков этих предложений. Процедуры, в свою очередь, также должны быть 89 сгруппированы в соответствии с именами предикатных символов заголовков входящих в них предложений. Считается, что метапредложения не входят в состав каких-либо процедур, однако такие метапредложения, в заголовке которых присутствует предикатный символ, и в качестве этого предикатного символа задан символ, должны быть сгруппированы с предложениями с таким же именем предикатного символа заголовков. Примечание. Предложения, в заголовке которых задано объявление функции, группируются по общим правилам, вместе с другими предложениями класса. При этом арность предикатных символов заголовков этих предложений определяется без учёта терма или выражения, возвращаемого функцией. 6.1. АТОМАРНЫЕ ФОРМУЛЫ Атомарными формулами (атомами) в языке являются следующие обозначения: атом = простой_атом | бинарное_отношение | объявление_функции 6.1.1. ПРОСТЫЕ АТОМЫ Простой атом - это функтор с соответствующим количеством аргументов, недоопределённое множество или переменная: простой_атом = функтор [ "(" [ термы_и_выражения [ "*" ] ] ")" ] | недоопределённое_множество | метапеременная Последний аргумент атомарной формулы может быть помечен "*" только тогда, когда он является переменной. В этом случае атомарная формула обозначает предикат с переменным числом аргументов ("предикат переменной арности"), а помеченная переменная - список аргументов, не определённых явно в составе атомарной формулы. Во время трансляции арность такого предиката неопределена, однако в ходе исполнения программы эта атомарная формула может быть унифицирована, в общем случае, с атомом любой арности большей или равной R-1, где R - количество аргументов, заданных в составе рассматриваемого предиката (включая помеченную переменную). Переменные, помеченные "*", а также переменные, используемые в качестве атомов и функторов, называются "метапеременными". Предикаты переменной арности, метапеременные, используемые в качестве атомов, а также атомы, в качестве функторов которых используются метапеременные, называются "метапредикатами" ("метаатомами"). Переменная в атомарной формуле подцели предложения может быть помечена "*" лишь в том случае, если она таким же образом помечена в заголовке предложения и не является анонимной переменной "_". Для обозначения списка аргументов предиката переменной арности не разрешается использовать метафункторы. Атомарная формула вида 90 A0{x1:A1,x2:A2,...,xn:An|Rest} эквивалентна ''({0:A0,x1:A1,x2:A2,...,xn:An|Rest}), где '' - символ, состоящий из пустой цепочки графем. Пример. Предложение, имитирующее правило 2-го порядка. Для обозначения данных в примере используются недоопределённые множества, в состав которых входит признак чётности "is_even". P{is_even:'any'|Rest}:P{is_even:'yes'|Rest}, P{is_even:'no'|Rest}. Приведённое утверждение означает, что любой предикат P является истинным при чётных и нечётных значениях аргумента, если его истинность удаётся доказать отдельно для чётных и нечётных значений этого аргумента. 6.1.2. БИНАРНЫЕ ОТНОШЕНИЯ "Бинарным отношением" называется атомарная формула, состоящая из двух аргументов, соединённых оператором отношения: бинарное_отношение = терм_или_выражение оператор_отношения терм_или_выражение В качестве знаков операций в бинарных отношениях используются имена встроенных предикатов '==' и ':=', а также некоторые знаки операций сравнения: оператор_отношения = "==" | ":=" | "<" | ">" | "<>" | "<=" | ">=" Бинарное отношение, в состав которого входит такой знак операции, эквивалентно обозначению вида функтор(аргумент1,аргумент2), где функтор - знак операции, заключённый в апострофы, аргумент1 и аргумент2 операнды, стоящие соответственно слева и справа от знака операции. 6.1.3. ОБЪЯВЛЕНИЯ ФУНКЦИЙ Функциями называется разновидность предикатов, предназначенная для имитации подпрограмм-функций, возвращающих выходное значение. Определение функций осуществляется с помощью специальных синтаксических конструкций, называемых "объявлениями функций". объявление_функции = простой_атом "=" терм_или_выражение В качестве простых атомов в составе объявлений функций не разрешается использовать метапеременные. В результате трансляции объявления функций преобразуются в предикаты. В ходе трансляции предложения, имитирующего объявление функции, p(A1,A2,...,An) = E :- Конъюнкция. , оно преобразуется к виду p(E,A1,A2,...,An):- Конъюнкция, S. При этом все вызовы функций S, входящие в состав терма E, выносятся в конец предложения, после подцелей "Конъюнкция". Вызовы функций S всегда помещаются после любых других вызовов функций, вынесенных в конец предложения из его заголовка. 91 6.2. ПОДЦЕЛИ ПРЕДЛОЖЕНИЙ Подцелями предложения служат вызовы предикатов. "Вызовом предиката" называется синтаксическая конструкция, определяющая экземпляр класса, в котором этот вызов должен быть исполнен, тип вызова (ближний или дальний), а также атомарную формулу вызова. Различаются ближние и дальние, а также простые и акторные вызовы предикатов. Вызов предиката называется "дальним", если в подцели явным образом (с помощью переменной или атрибута) указан мир, в котором он должен быть исполнен. Если соответствующий мир не указан, вызов предиката называется "ближним". Исполнение ближнего вызова осуществляется в том же самом мире, в котором исполняется рассматриваемое предложение. Акторными вызовами предикатов называются подцели предложений, определяющие акторы. Если про вызов предиката не сказано, что он является акторным, такой вызов называется (является) простым. подцель = простая_подцель | бинарное_отношение | "[" [ термы_и_выражения ] "]" | "!" простая_подцель = [ [ целевой_параметр ] инфикс_подцели ] простой_атом инфикс_подцели = "?" | "<<" | "<-" Если инфикс подцели равен "<<" или "<-", в качестве простого атома этой подцели не разрешается использовать метапеременные. Подцель [V1,...,Vk] обозначает встроенный управляющий оператор copy(V1,...,Vk). Подцель "!" обозначает встроенный управляющий оператор отсечения '!'. Пример. Правильно построенные предложения: clause_1(M,N,J):M * 2 + N < 7 - ? f(J), -- простой ближний вызов console ? write("N=",N). -- простой дальний вызов clause_2(K,1,N,L*):check(K, slot ? p(N) ), -- простой ближний вызов ? p(N,7,L). -- простой ближний вызов 6.2.1. ВЫЗОВЫ ФУНКЦИЙ "Вызовом функции" называется синтаксическая конструкция, имитирующая вызов подпрограммы-функции. В составе предложений разрешается использовать следующие вызовы функций: вызов_функции_в_предложении = [ целевой_параметр ] "?" простой_атом | целевой_параметр "[" термы_и_выражения "]" В качестве простых атомов в составе вызовов функций не разрешается использовать метапеременные. 92 Если в составе вызова функции целевой параметр не задан явно, целевым параметром считается предопределённый атрибут self. В результате трансляции вызовы функций вида W ? p(A1,A2,...,An) преобразуются в вызовы предикатов вида W ? p(R,A1,A2,...,An) , а вызовы функций вида X [A1,A2,...,An] - в вызовы предикатов вида X ? element(R,A1,A2,...,An) , где R - некоторая уникальная переменная, обозначающая результат функции и помещаемая на место транслируемого вызова функции. В случае если вызов функции используется в составе некоторой подцели SA, вызов предиката SB, соответствующий этому вызову функции, добавляется в состав предложения перед подцелью SA. При этом гарантируется, что подцель SB будет помещена перед любой другой подцелью, в состав которой войдёт переменная R, обозначающая результат рассматриваемого вызова функции, но после всех подцелей, находившихся перед подцелью SA в исходном предложении. Если вызов функции используется в заголовке предложения вида Заголовок:- Конъюнкция. и не является составной частью другого вызова функции, вызов предиката S, соответствующий этому вызову функции, добавляется в состав предложения после конъюнкции подцелей "Конъюнкция": Заголовок:- Конъюнкция, S. Если вызов функции FC1 в заголовке предложения входит в состав другого вызова функции FC2, в этом случае FC1 рассматривается как вызов функции, входящий в состав подцелей предложения, поставленных в соответствие вызову функции FC2. Примечание. В соответствии с правилами исполнения вызова предиката, определёнными в разделе 6.3.1, вызовами функций считаются также такие подцели метапредложений, атомарная формула которых является метапеременной, при условии что рассматриваемое метапредложение поставлено в соответствие вызову функции. Пример. Определение функции append. Определение функции append, добавляющей элементы в конец списка, append([],L) = L. append([H|L1],L2) = [H | ?append(L1,L2)]. соответствует процедуре вида append(L,[],L). append(R0,[H|L1],L2):append(R1,L1,L2), R0 == [H | R1]. 6.2.2. ВЫРАЖЕНИЯ Выражение - это видоизменённый вызов функции: 93 выражение = [ выражение аддитивный_оператор ] слагаемое | выражение аддитивный_оператор терм слагаемое = [ слагаемое мультипликативный_оператор ] множитель | слагаемое мультипликативный_оператор терм множитель = [ "-" ] "(" выражение ")" Для построения выражений используется ограниченный набор знаков операций, в состав которого входят следующие математические символы: аддитивный_оператор = "+" | "-" мультипликативный_оператор = "*" | "/" Выражение, построенное с помощью инфиксного знака операции, эквивалентно вызову функции вида ?функтор(аргумент1,аргумент2), где функтор - знак операции, заключённый в апострофы, аргумент1 и аргумент2 операнды, стоящие соответственно слева и справа от знака операции. Выражение, построенное с помощью префиксного знака операции "-", эквивалентно вызову функции вида ?'-'(аргумент), аргументом которой является операнд выражения. Пример. Правильно построенные выражения: 1+H*-(E)/4+(W+"A4"-319e0), 'f'*X+(7-"t")-'r' 6.3. СТРАТЕГИЯ УПРАВЛЕНИЯ Стратегия управления Акторного Пролога ("акторный механизм") является расширением стандартной стратегии управления ("поиск слева направо в глубину с возвратом"), соответствующей текстуальному упорядочению процедур и вызовов предикатов. Отличиями акторного механизма от стандартной стратегии управления являются возможности повторного доказательства акторов, а также задержки исполнения подцелей. 6.3.1. ИСПОЛНЕНИЕ ВЫЗОВА ПРЕДИКАТА Общая схема исполнения вызова предиката (исполнения предиката) включает следующие действия: Выбор предложения, заголовок которого: Cодержит предикатный символ, совпадающий с именем вызывающего предиката. В случае исполнения вызова функции, запрещается выбирать предложения, заголовки которых не являются объявлениями функций. Является метапеременной. Содержит метафунктор в качестве предикатного символа. Если следом за выбранным предложением в рассматриваемом мире расположены ещё не исследованные предложения, в исполняемом процессе создаётся новая точка выбора, обозначающая поиск иных предложений, соответствующих условиям пункта 1. Построение копии предложения, отличающейся от выбранного предложения тем, что все его переменные заменяются новыми уникальными именами, а все 94 атрибуты заменяются значениями слотов мира, которому принадлежит предложение. Исполнение построенного предложения. При этом исполнение вызовов предикатов различных типов осуществляется в соответствии со следующими дополнительными правилами и исключениями: Если значением целевого параметра в дальнем вызове предиката является спейсер #, исполнение подцели заканчивается успехом (без какого-либо связывания переменных). Если значением целевого параметра в дальнем вызове предиката является элемент данных, исполнение подцели осуществляется непосредственно в том мире, в котором исполняется рассматриваемое предложение. При этом целевой параметр добавляется в исполняемый предикат в качестве дополнительного аргумента перед другими аргументам предиката, но после аргумента, обозначающего возвращаемое значение функции (в случае если исполняемая подцель является вызовом функции). Исполнение дальних вызовов предикатов с инфиксами "<<" и "<-" всегда заканчивается успехом и заключается в подготовке соответствующих прямых сообщений, передача которых откладывается до (успешного) завершения рассматриваемой фазы исполнения процесса. Инфикс "<<" обозначает информационные прямые сообщения, а инфикс "<-" - переключающие прямые сообщения. Исполнение дальних вызовов предикатов, не имеющих инфикса "<<" или "<-", в мирах, не принадлежащих процессу, в котором исполняется рассматриваемое предложение, невозможно и заканчивается неудачей. Исполнение акторных вызовов предикатов осуществляется в соответствии с общей схемой, но при этом дополнительным необходимым условием успешного завершения доказательства любого актора P процесса G является существование производных значений общих переменных процесса G. Для того чтобы обеспечить существование требуемых производных значений, в момент (успешного) завершения доказательства предиката P осуществляется согласование акторов процесса G. Доказательство актора P считается успешным в том и только в том случае, если завершаются успехом исполнение соответствующего предиката, а также последующее согласование акторов, в противном случае происходит откат. Если атомарная формула исполняемого вызова предиката в составе метапредложения является метапеременной, данная подцель считается вызовом функции, если (и только если) рассматриваемое метапредложение поставлено в соответствие вызову функции. Действия, осуществляемые при исполнении предопределённых предикатов и встроенных управляющих операторов рассмотрены в главе 8. Подцели копии предложения, построенной во время исполнения предиката, называются "подцелями доказательства". 6.3.2. ИСПОЛНЕНИЕ ПРЕДЛОЖЕНИЯ 95 Будем говорить, что переменная или слот имеют "задерживающее" ("отключающее") значение, если они несвязаны или их значение равно спейсеру #. Задержанными подцелями называются подцели доказательства, исполнение которых было отложено механизмом задержки исполнения подцелей. Списком задержанных подцелей называется вспомогательный список подцелей доказательства, используемый механизмом задержки исполнения подцелей. Исполнение предложения включает: Унификацию функтора и аргументов исполняемого вызова предиката с функтором и аргументами заголовка предложения или унификацию вызова предиката с метапеременной (если заголовком предложения является метапеременная). В случае если заголовок предложения является объявлением функции, а исполняемый вызов предиката не является вызовом функции, перед унификацией в начало списка аргументов исполняемого вызова добавляется фиктивный аргумент - анонимная переменная "_". Пересмотр списка задержанных подцелей, осуществляемый механизмом задержки исполнения подцелей. Исполнение соответствующих подцелей доказательства. При этом подцели доказательства, целевые параметры которых имеют задерживающие значения, пропускаются и добавляются в список задержанных подцелей. Исполнение предложения заканчивается успехом тогда и только тогда, когда успехом заканчиваются все три названные операции. В случае унификации функтора исполняемого вызова предиката с метафунктором, значением метафунктора становится соответствующий терм символ. В случае унификации исполняемого вызова предиката с метапеременной, значением метапеременной становится соответствующий терм - символ или структура. Однако при использовании названных метафункторов и метаатомов в качестве предикатных символов и предикатов подцелей, они рассматриваются, соответственно, как правильно построенные имена предикатов и предикаты. Вызов предопределённых предикатов и встроенных управляющих операторов с помощью метаатомов невозможен и всегда заканчивается неудачей. Примечание. Метаатом в заголовке метапредложения может быть только символом или только структурой, потому что, в соответствии с семантикой языка, количество аргументов структуры не может быть меньше единицы. 6.3.3. МЕХАНИЗМ ЗАДЕРЖКИ ИСПОЛНЕНИЯ Механизмом задержки исполнения подцелей называется вспомогательная стратегия управления, откладывающая исполнение выделенных подцелей до тех пор, пока не будет вычислена некоторая информация, необходимая для корректного исполнения этих подцелей. Пересмотр списка задержанных подцелей осуществляется следующим образом: Элементы списка просматриваются в том порядке, в котором они были в него добавлены. При обнаружении каждого элемента списка, значение целевого параметра которого не является задерживающим, найденная подцель исключается из рассматриваемого списка и исполняется. 96 Считается, что на каждой фазе исполнения процесса используется новый список задержанных подцелей. В начале фазы список задержанных подцелей является пустым. 6.3.4. ОТКАТ ПРОГРАММЫ Откатом называется возобновление исполнения процесса, начиная с последней (неустранённой оператором отсечения) точки выбора. Откат выполняется автоматически в случае неудачи какой-либо операции, осуществляемой в ходе исполнения предложения. В результате отката программы осуществляется восстановление состояний акторов процесса, в котором произошёл откат, на момент прохождения последней (неустранённой оператором отсечения) точки выбора (в том числе отмена всех связываний и сцеплений переменных, произошедших в акторах с момента прохождения этой точки, отмена нейтрализации и повторных доказательств акторов, а также отмена всех изменений, внесённых в список задержанных подцелей). В результате отката отменяются все прямые сообщения, подготовленные после прохождения последней неустранённой точки выбора для передачи из рассматриваемого процесса. 7. АКТОРЫ И ПОВТОРНЫЕ ДОКАЗАТЕЛЬСТВА Повторное доказательство акторов в Акторном Прологе автоматически поддерживает корректность логического вывода при использовании разрушающего присваивания и параллельных процессов. 7.1. АКТОРЫ Актором называется подцель доказательства, соответствующая акторному вызову предиката. Актор Q называется "вложенным" по отношению к актору P, если эти акторы принадлежат одному процессу, и доказательство актора Q, результаты которого в данный момент не отменены, происходит (произошло) в ходе доказательства актора P. Нейтрализацией актора называется отмена всех результатов его доказательства, за исключением результатов доказательства вложенных по отношению к нему акторов. Повторным доказательством актора называется повторение доказательства актора с самого начала. Актор может находиться в одном из трёх состояний: "активный" актор; "доказанный" актор; "нейтральный" актор. Актор P, доказательство которого происходит (произошло) в ходе некоторой фазы F исполнения процесса G, считается (называется) "активным" с момента начала его доказательства до завершения рассматриваемой фазы F. В случае успешного завершения доказательства актора P, а также успешного завершения фазы F, 97 актор P считается "доказанным" с момента завершения фазы F до его (возможной) нейтрализации на одной из последующих фаз исполнения процесса G. Актор, предыдущее доказательство которого отменено, а повторное доказательство ещё не началось, называется "нейтральным". Перевод актора в активное состояние называется "активизацией" актора. 7.2. ОБЩИЕ ПЕРЕМЕННЫЕ Будем говорить, что актор P "использует" переменную V (или что переменная V "соответствует", "принадлежит" актору P), если переменная V входит в состав подцели доказательства P или какой-либо другой подцели доказательства Q, построенной в ходе: текущего доказательства подцели P, если исполнение актора ещё не закончено, последнего завершившегося успехом доказательства актора P, если исполнение актора закончено, и он не является нейтральным, не считая тех подцелей доказательства Q, которые были построены в ходе доказательства акторов, вложенных по отношению к P. "Общей" называется переменная, которая используется (или может быть использована) несколькими акторами. Каждый актор хранит свои собственные ("локальные") значения общих переменных. Актуальными значениями общих переменных некоторого процесса называются значения, которые можно получить, унифицировав локальные значения всех общих переменных, соответствующих активным акторам этого процесса. Во время унификации термов (в ходе исполнения процесса G) происходит замена тех (и только тех) вхождений переменных, которые соответствуют активным акторам (процесса G). Таким образом, локальные значения общих переменных любого активного актора процесса всегда равны актуальным значениям общих переменных этого процесса. Производными значениями общих переменных некоторого процесса называются значения, которые можно получить (если они существуют), унифицировав локальные значения общих переменных всех активных и доказанных акторов этого процесса. В случае если общая переменная процесса (например, некоторый порт процесса) не соответствует ни одному из активных или доказанных акторов, её производным значением считается анонимная переменная "_". Акторы процесса считаются согласованными между собой, если: Все акторы, принадлежащие процессу, хотя бы один раз были доказаны. Существуют производные значения общих переменных этого процесса. "Фиксированными" значениями общих переменных называются значения этих переменных, в составе которых все несвязанные переменные заменены спейсером #. "Фиксированием" терма называется заменена спейсером всех несвязанных переменных в составе терма. В соответствии с семантикой Акторного Пролога, фиксирование не распространяется на переменные в составе миров, являющихся компонентами фиксируемого терма. Глобальными операциями (с общими переменными) называются операции, в которых используются локальные значения, принадлежащие активным и доказанным акторам некоторого процесса - сопоставление локальных значений 98 общих переменных, актуализация производных значений общих переменных, передача потоковых сообщений из процесса. В случае если некоторая общая переменная используется для передачи потоковых сообщений между процессами, в соответствие этой переменной ставятся: "Глобальное" значение - некоторый терм. Сорт текущего глобального значения - вспомогательное логическое значение: "защищённое" или "незащищённое". Производитель текущего глобального значения - процесс, построивший текущее глобальное значение. Эти атрибуты являются едиными для всех процессов, передающих и принимающих потоковые сообщения через рассматриваемую переменную. Считается, что изначально глобальное значение общей переменной равно пустому значению сорта "незащищённое", производителем которого является некоторый (уникальный) процесс, не совпадающий ни с одним из процессов программы. 7.2.1. ПОСТРОЕНИЕ ОБЩИХ ПЕРЕМЕННЫХ Общие переменные создаются автоматически в составе значений слотов во время формирования экземпляров классов. При построении общих переменных выполняются следующие правила: Если в составе инициализатора аргумента конструктора (явным образом) задан атрибут, то в качестве соответствующего ему значения слота берётся начальное значение этого слота (созданное во время построения слота), вместе с соответствующими ему общими переменными. Если в определении атрибутов класса в составе некоторого инициализатора аргумента некоторого конструктора явным образом задана некоторая переменная X, то в качестве значения X берётся соответствующая ей общая переменная, созданная в ходе построения слотов экземпляра класса. Примечание. Указанные правила построения общих переменных выполняются даже в том случае, если значение рассматриваемого слота или переменной ранее уже было конкретизировано в каких-либо акторах. 7.3. СОГЛАСОВАНИЕ АКТОРОВ ПРОЦЕССА Согласованием акторов процесса G называются действия, осуществляемые для обеспечения согласованности акторов процесса. Согласование акторов процесса необходимо для того, чтобы обеспечить существование производных значений общих переменных этого процесса. Согласование акторов включает: сопоставление локальных значений общих переменных акторов процесса; повторное доказательство акторов, нейтрализованных в ходе проведённого сопоставления локальных значений. 7.3.1. СОПОСТАВЛЕНИЕ ЛОКАЛЬНЫХ ЗНАЧЕНИЙ На первом этапе согласования акторов сопоставляются локальные значения общих переменных, соответствующие различным акторам процесса G. Сопоставление локальных значений общих переменных осуществляется следующим образом: 99 Вычисляются фиксированные актуальные значения общих переменных процесса G. Нейтрализуются все акторы процесса G, находящиеся в состоянии "доказанный", локальные значения которых не могут быть унифицированы с фиксированными актуальными значениями процесса. Порядок нейтрализации акторов в языке не определён. 7.3.2. ИСПОЛНЕНИЕ ПОВТОРНЫХ ДОКАЗАТЕЛЬСТВ После сопоставления локальных значений общих переменных автоматически вызывается повторное доказательство всех нейтральных акторов процесса G, за исключением акторов-представителей портов процесса (см. свойства акторовпредставителей в разделе 5.2.2). Порядок исполнения повторных доказательств акторов в языке не определён. Согласование акторов считается успешным в том и только в том случае, если завершаются успехом все повторные доказательства. Примечание. В результате повторного доказательства недетерминированного актора, могут возникать новые точки выбора. 7.4. СОГЛАСОВАНИЕ ПРОЦЕССОВ Согласованием процессов называются действия, осуществляемые для обеспечения согласованности процессов. Согласование процессов происходит с помощью обмена асинхронными сообщениями. Сообщением называется некоторое количество информации, передаваемое между процессами, представляющее для них единое целое. Действия, реализующие распространение информации из одного процесса в другие, называются передачей сообщений. В общем случае, передача сообщений из некоторого процесса может осуществляться каждый раз после завершения очередной фазы его исполнения. Действия, осуществляемые процессом в случае получения сообщения, называются обработкой сообщения. Обработка сообщения является отдельной фазой исполнения процесса. Процесс не принимает и не посылает никакие сообщения до завершения очередной фазы своего исполнения. 7.4.1. КЛАССИФИКАЦИЯ СООБЩЕНИЙ В языке различаются прямые и потоковые, а также переключающие и информационные сообщения. Прямые и потоковые сообщения имеют следующие принципиальные отличия: Прямые сообщения передаются непосредственно от одного процесса к другому (в виде дальнего вызова предиката), а потоковые сообщения - от одного процесса ко многим (в виде значения общей переменной). Прямые сообщения никогда не теряются при передаче, в то время как потоковые сообщения, которые процесс ещё не успел обработать, могут быть заменены более новой информацией. Отличие переключающих и информационных сообщений состоит в том что: В результате обработки переключающего сообщения процесс может перейти в состояние "доказанный", "неудачный" или (остаться в состоянии) 100 "сформированный", в то время как после обработки информационного сообщения процесс всегда оказывается в состоянии "доказанный". В отличие от переключающих сообщений, обработка информационных сообщений процессом откладывается до тех пор, пока он не окажется в состоянии "доказанный". В случае если обработка переключающего сообщения завершилась неудачей или исключительной ситуацией, процесс переходит в состояние "неудачный" или если перед началом обработки сообщения он находился в состоянии "сформированный" - в состояние "сформированный". В случае если неудачей или исключительной ситуацией завершилась обработка информационного сообщения, происходит "поглощение" сообщения: состояние процесса восстанавливается на момент, предшествовавший обработке сообщения, а само сообщение отменяется. В языке используются переключающие потоковые, а также прямые информационные и прямые переключающие сообщения. Порядок обработки сообщений в языке не определён, однако в случае если процессу необходимо обработать несколько сообщений, прямые сообщения всегда обрабатываются после потоковых, а информационные, при прочих равных условиях, после переключающих. Если процесс, получивший сообщения является целевым процессом некоторого резидента, вычисление значений функции резидента, при прочих равных условиях, осуществляется после обработки переключающих сообщений, но до обработки информационных сообщений. Гарантируется, что при условии возможности обработки сообщения, оно обязательно будет обработано через некоторый конечный промежуток времени. 7.4.2. ПРЯМЫЕ СООБЩЕНИЯ Прямым сообщением называется сообщение, реализующее исполнение дальнего вызова предиката из одного процесса в другом. Передача прямых сообщений, подготовленных в результате исполнения дальних вызовов предикатов в течение некоторой фазы F исполнения процесса G, происходит в случае успешного завершения рассматриваемой фазы F. Перед передачей прямых сообщений все несвязанные переменные в их составе заменяются соответствующими фиксированными производными значениями процесса G. Обработка прямого сообщения (принимающим) процессом H включает следующие действия: Осуществляется активизация всех портов S, текущее значение D которых не является пустым, а производитель текущего значения не равен H, таких что: порт S относится к сорту "отключающий"; порт S относится к сорту "простой", а сорт текущего значения D равен "защищённое". В соответствующем мире процесса H, указанном в дальнем вызове предиката P в составе обрабатываемого сообщения, (временно) создаётся и доказывается новый актор Q, соответствующий вызову предиката P. В частности, если процесс H находится в состоянии "сформированный", то в момент (успешного) завершения доказательства предиката P, во всех мирах этого процесса, сформированных в 101 ходе исполнения конструктора процесса H, (для согласования акторов процесса H) создаются и доказываются новые акторы, представленные акторными вызовами предиката goal. В случае успешного завершения доказательства актора Q, обработка сообщения считается успешно завершённой. В случае если доказательство актора Q завершилось неудачей или исключительной ситуацией, обработка сообщения прекращается. После завершения обработки сообщения, актор Q прекращает существование. Обработка прямых сообщений процессом допускается лишь в том случае, если он уже обработал все полученные им потоковые сообщения, и, следовательно, не имеет несогласованных портов. Примечание. Рекомендуемые графические обозначения прямых сообщений: а) Информационное прямое сообщение. +------------+ +-------------+ | | | | | Источник |......>>..... | Приёмник | | | | | +------------+ +-------------+ б) Переключающее прямое сообщение. +------------+ +--------------+ | | | | | Источник |......->......| Приёмник | | | | | +------------+ +--------------+ 8. ВСТРОЕННЫЕ ПРЕДИКАТЫ И ОПЕРАТОРЫ Встроенными предикатами языка являются goal(), alarm(E), ''(Set) и element(Value,I1,...,Ik), определяемые в тексте программы, а также предопределённые предикаты: '=='(V1,...,Vk) - унифицировать термы; ':='(V1,...,Vk) - разрушающее присваивание; true[(...)] - истина; fail - ложь (неудача). Кроме того, в языке определены встроенные управляющие операторы, использование которых может нарушить полноту программы относительно её декларативной семантики: copy(V1,...,Vk) - актуализация производных значений; '!' - отсечение; break[(E)] - вызов исключительной ситуации; spypoint(...) - обращение к отладчику. Встроенный оператор отсечения устраняет все неисследованные пути (точки выбора), которые встретились с момента начала исполнения предиката, в соответствие которому было поставлено предложение, содержащее оператор. Результаты исполнения оператора "обращение к отладчику" должны быть определены в конкретной реализации языка. 102 В программе не допускается определение предикатов, имена которых совпадают с именами предопределённых предикатов и встроенных управляющих операторов. Не разрешается использование таких имён в качестве предикатных символов в акторных и дальних вызовах. Неверное число аргументов в предопределённых предикатах и встроенных управляющих операторах является синтаксической ошибкой. Пример. Использование оператора отсечения. Рассмотрим поведение фрагмента программы goal:write("<1>"), subgoal_a, write("<7>"). goal:write("<8>"). subgoal_a:write("<2>"), subgoal_b, !, write("<4>"), fail. subgoal_a:write("<6>"). -- отсечение subgoal_b:write("<3>"). subgoal_b:write("<5>"). Если убрать оператор отсечения, программа напечатает: <1><2><3><4><5><4><6><7> При наличии оператора отсечения будет напечатано: <1><2><3><4><8> 103 ОГЛАВЛЕНИЕ Лабораторная работа № 1 Основы логического программирования на Прологе..3 Лабораторная работа № 2 Программирование структур данных на Прологе……26 Лабораторная работа № 3 Среда программирования Visual Prolog………………35 Лабораторная работа № 4 Среда программирования Actor Prolog……………….56 104