Eduardo Costa
Visual Prolog 7.0 for Tyros Перевод с английского
Сафронов Марк a.k.a. ‐S‐ 20.07.2007
Upda...
99 downloads
346 Views
5MB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
Eduardo Costa
Visual Prolog 7.0 for Tyros Перевод с английского
Сафронов Марк a.k.a. ‐S‐ 20.07.2007
Update 1 Revision 73
Комментарий переводчика Ад кромешный ‐ переводить подобные тексты. Во‐первых, большую часть терминов хочется ос‐ тавить вообще без перевода, потому что программисты все равно пользуются сленгом, основанным на английском варианте этих терминов, а общепринятый перевод даже если и есть, то не склоняет‐ ся, или в некоторых фразах выглядит как бред сумасшедшего. Во‐вторых, все время необходимо ре‐ шать классическую проблему перевода: нужно ли переводить дословно, или нужно вместе с перево‐ дом ещё и перерабатывать стилистику текста, чтобы он соответствовал общепринятой стилистике русского языка. Конечно же, можно определить т. н. «идеальный» перевод: текст, написанный так, как его бы написал оригинальный автор, если бы свободно владел языком перевода. Но, чтобы перевести текст так качественно, необходимо быть этим автором, то есть, писать книги на нескольких языках. =) Ещё, я начинал переводить эту книгу, когда она называлась Visual Prolog 6.3 for Tyros, и предна‐ значалась, соответственно, для версии, 6.3,пользуясь в то же самое время средой разработки Visual Prolog 7.0. Это было не самым приятным развлечением, так как некоторые программы из книги ра‐ ботали не так, как надо, или, чаще всего, не работали вообще, о чём мне приходилось писать в ком‐ ментариях. Выход обновлённого текста избавил меня от этих проблем, но произошёл, уже когда пе‐ ревод исходного текста был закончен, что потребовало от меня ещё раз переворошить всю работу. :) Теперь конкретно. Я не переводил имена собственные нигде в тексте. Исключение сделано для имен людей, для которых я привожу русскую транскрипцию и оригинальное английское написание в скобках. Оформление по большей части оставлено авторское, за исключением того, что я позволил себе «продолжить» оформление там, где, как мне показалось, оно должно было быть; это относится к курсивному выделению пунктов меню и моноширинному шрифту листингов кода. Для терминов, подобным project tree, code expert и package, принадлежащих конкретно теме текста, я использовал собственный перевод, так как текст стал бы трудночитабельным, если я бы оставил их без перевода.. Для терминов, имеющих общеупотребительный эквивалент на русском языке, я пользовался этим эквивалентом, даже если, возможно, значение эквивалента отличалось от значения термина на анг‐ лийском языке (тем более что эти термины большей частью из жаргона программистов). Я старался сохранить авторскую стилистику нетронутой – что было довольно легко, так как я ис‐ пользовал дословный перевод везде, где можно – но в некоторых местах пришлось все‐таки пере‐ строить предложения довольно значительно, так как в дословном переводе с небольшими измене‐ ниями, только для простого соответствия правилам русского языка, эти предложения становились похожими на горячечный бред. В некоторых местах я позволил себе полностью изменить некоторые фразы, это касается, в частности, подписей под некоторыми рисунками. Теперь техническая информация. В конце почти каждой главы имеется раздел Examples ‐ приме‐ ры, в котором перечисляются программы, исходные коды которых приложены к этому тексту в от‐ дельном архиве. Также, книга для версии 7.0 является обновлением такой же книги для версии Visual Prolog 6.3 – Visual Prolog 6.3 for Tyros. Это означало, что некоторые изображения окон IDE седьмой версии имели некоторые расхождения с окнами шестой версии, а именно, изображения конкретно для Visual Prolog 7.0 сняты с окон в классическом стиле оформления Windows, в то время как изображения, не требовавшие обновления, автор оставил как были – в стиле WinXP‐Standard (Blue). Я позволил себе в свою очередь обновить эти изображения, чтобы книга выглядела более целостностной. Из оригинального текста полностью удалены перечень рисунков, следовавший сразу за содер‐ жанием, и Index (список использованных в тексте терминов), в самом конце.
2
Предисловие Каждый из нас имел опыт общения с заумными книгами и статьями, которые больше скрывают, чем показывают вклад автора. Например, Пьер‐Симон, маркиз де Лаплас (PierreSimon, Le Marquis de Laplace), вставил только один рисунок в свою «Небесную Механику» (Méchanique Celeste), а Пеано (Peano), как Мел Гибсон (Mel Gibson), написал свою Arithmetices Principia Novo Methodo Exposita на ла‐ тыни! Я думаю, парень, придумавший публиковать книги «для чайников», сделал много денег. Он понимал, что большинство людей предпочтут быть названными «чайником», чем повстречаться с Лапласом. Но так как вы не хотите ни быть названным «чайником», ни иметь дело с маркизом, на‐ звание этой книги Prolog for tyros1, не for dummies. Eduardo Costa
Uxoribus Amantissimis Liberisque Pientissimis
Слово tyro заимствовано из латыни: tyro=tiro – лат. новичок. Таким образом, книга называется «Visual
1
Prolog для новичков» (здесь и далее прим. пер.).
3
Содержание Комментарий переводчика ___________________________________________________ 2 Предисловие ________________________________________________________________ 3 Содержание ________________________________________________________________ 4 Глава 1: 1.1.
Введение __________________________________________________________ 7 Создание проекта в Visual Prolog ______________________________________________ 7
1.1.1. Создание нового GUI проекта _____________________________________________________ 7 1.1.2. Компиляция и запуск программы __________________________________________________ 8
1.2.
Глава 2:
Примеры __________________________________________________________________ 8
Формы ___________________________________________________________ 9
2.1.
Создание формы ___________________________________________________________ 9
2.2.
Включение панели задач: e.g. пункт File/New __________________________________ 11
2.3.
Добавление кода к предмету из дерева проекта в CodeExpert ___________________ 12
2.4.
Примеры _________________________________________________________________ 12
Глава 3:
Mouse events _____________________________________________________ 14
3.1.
Добавим код к событию MouseDown _________________________________________ 14
3.2.
onPaint ___________________________________________________________________ 14
3.3.
Примеры _________________________________________________________________ 15
Глава 4:
Поменьше картинок ______________________________________________ 16
4.1.
Панель задач ______________________________________________________________ 16
4.2.
Дерево проекта ___________________________________________________________ 16
4.2.1. Code Expert ____________________________________________________________________ 17
4.3.
Создание элемента проекта _________________________________________________ 18
4.4.
Панель задач. _____________________________________________________________ 19
4.5.
Сыграем снова. ____________________________________________________________ 19
4.6.
Создание нового класса ____________________________________________________ 20
4.7.
Содержимое строки ввода __________________________________________________ 22
4.8.
Примеры _________________________________________________________________ 23
Глава 5:
Предложения Хорна _______________________________________________ 24
5.1.
Функции __________________________________________________________________ 24
5.2.
Предикаты ________________________________________________________________ 24
5.3.
Решения __________________________________________________________________ 25
5.4.
Множественные решения __________________________________________________ 28
5.4.1. Программа, использующая предикаты с множественными решениями _________________ 28
5.5.
Логические связки. ________________________________________________________ 30
5.6.
Импликация ______________________________________________________________ 30
5.7.
Предложения Хорна. _______________________________________________________ 31 4
5.8.
Объявления _______________________________________________________________ 31
5.8.1. Объявления режимов детерминизма ______________________________________________ 32
5.9.
Предикаты рисования ______________________________________________________ 33
5.10. GDI объект ________________________________________________________________ 33 5.11. Примеры _________________________________________________________________ 34
Глава 6:
Консольные приложения ___________________________________________ 35
6.1.
Отсечение ________________________________________________________________ 35
6.2.
Списки ___________________________________________________________________ 36
6.3.
Схемы обработки списков __________________________________________________ 39
6.4.
Операции со строками. _____________________________________________________ 40
Глава 7:
Грамматика _____________________________________________________ 46
7.1.
Грамматический анализ ____________________________________________________ 46
7.2.
Грамматический синтез ____________________________________________________ 47
7.3.
Почему Пролог? ___________________________________________________________ 48
7.4.
Примеры _________________________________________________________________ 49
Глава 8:
Рисование ________________________________________________________ 50
8.1.
onPainting ________________________________________________________________ 50
8.2.
Пользовательские элементы управления _____________________________________ 52
Глава 9:
Типы данных _____________________________________________________ 54
9.1.
Примитивные типы данных _________________________________________________ 54
9.2.
Множества _______________________________________________________________ 55
9.3.
Множества чисел __________________________________________________________ 55
9.4.
Иррациональные числа ____________________________________________________ 57
9.5.
Действительные числа _____________________________________________________ 58
9.6.
Форматирование __________________________________________________________ 58
9.7.
Домены __________________________________________________________________ 59
9.7.1. Списки ________________________________________________________________________ 59 9.7.2. Функторы _____________________________________________________________________ 60
Глава 10:
Как решать это в Прологе _______________________________________ 63
10.1. Утилиты __________________________________________________________________ 63
Глава 11:
Факты ________________________________________________________ 75
11.1. Класс file _________________________________________________________________ 77 11.1.1. Чтение и вывод строки ________________________________________________________ 77
Глава 12:
Классы и объекты _______________________________________________ 79
12.1. Факты объектов ___________________________________________________________ 80
Глава 13:
Giuseppe Peano __________________________________________________ 82
13.1. Черепашья графика ________________________________________________________ 82 13.2. Рекурсия _________________________________________________________________ 85
5
13.3. Кривая Пеано _____________________________________________________________ 86 13.4. Latino Sine Flexione _________________________________________________________ 87 13.5. Примеры _________________________________________________________________ 90
Глава 14:
L‐системы _____________________________________________________ 92
14.1. Класс draw ________________________________________________________________ 92 14.2. Примеры _________________________________________________________________ 94
Глава 15:
Игры __________________________________________________________ 95
15.1. Объектные факты ________________________________________________________ 100
Глава 16:
Анимация _____________________________________________________ 103
16.1. dopaint __________________________________________________________________ 103 16.2. Управление таймером ____________________________________________________ 104 16.3. Как программа работает ___________________________________________________ 104
Глава 17:
Текстовый редактор ___________________________________________ 106
17.1. Сохранение и загрузка файлов _____________________________________________ 107
Глава 18:
Печать _______________________________________________________ 109
Глава 19:
Баги __________________________________________________________ 111
19.1. Ошибки типа _____________________________________________________________ 111 19.2. Не‐процедура внутри процедуры ___________________________________________ 112 19.3. Неопределённые условия. _________________________________________________ 113 19.4. Невозможно определить тип _______________________________________________ 114 19.5. Шаблон потока ___________________________________________________________ 114
Глава 20:
Менеджер баз данных __________________________________________ 115
20.1. Менеджер базы данных ___________________________________________________ 115 20.2. Класс emails ______________________________________________________________ 117 20.3. Менеджер баз данных ____________________________________________________ 118
Глава 21:
Книги и статьи ________________________________________________ 120
21.1. Грамматика ______________________________________________________________ 120 21.2. Базы данных _____________________________________________________________ 120 21.3. Техники программирования _______________________________________________ 121
Библиография_____________________________________________________________ 122
6
Глава 1: Введение 1.1.
Создание проекта в Visual Prolog
Давайте создадим пустой проект, к которому позже добавим функциональности.
1.1.1. Создание нового GUI1 проекта Этот шаг довольно прост. Выберите команду Project/New из панели задач, как показано на рис. 1.1. Затем, заполните диалоговое окно Project Settings как на рис. 1.2. Нажмите кнопку Create, и перед вами появится окно дерева проекта (рис. 1.3).
Рисунок 1.1 Создание нового проекта
Рисунок 1.2 Настройки проекта
1
Graphical User Interface – графический пользовательский интерфейс.
7
Рисунок 1.3 Дерево проекта
1.1.2. Компиляция и запуск программы Для того, чтобы откомпилировать программу, выберите команду Build/Build из панели задач, как показано на рис. 1.4. Для запуска программы выберите Build/Execute из панели задач, и на экране появится окно, похожее на изображенное на рис. 1.5.
Рисунок 1.4 Сборка проекта
Рисунок 1.5 Пустое приложение
1.2.
Примеры
Примеры в этой главе показывают, как запускать приложение, и находятся в папке plot1.
8
Глава 2: Формы В этой главе вы добавите форму к пустому проекту, который вы создали в главе 1.
Рисунок 2.1 Добавление нового предмета к дереву проекта
Рисунок 2.2 Создание новой формы
2.1.
Создание формы
Чтобы создать форму, выберите команду File/New из панели задач, как на рис. 2.1. Выберите на левой панели пункт Form и заполните диалоговое окно Create Project Item как на рис. 2.2. Название новой формы – query. Удостоверьтесь, что вы поместили форму в корне дерева проекта. Это про‐ изойдет, если вы выбрали корневой каталог в дереве проекта, прежде чем нажать File/New.
9
Когда вы запрашиваете новую форму, Visual Prolog IDE1 показывает вам диалоговое окно под на‐ званием Form Properties (рис. 2.3). Вы можете принять установки по умолчанию в этом диалоговом окне. После диалогового окна Form Properties, IDE покажет вам прототип новой формы, как на рис. 2.4. Вы можете изменить размеры прямоугольника окна, сделав его немного больше прототипа. Чтобы сделать это, нажмите мышкой на нижнем правом углу формы и тяните его, как вы делаете, когда из‐ меняете размеры обычного окна.
Рисунок 2.3 Form Properties
Рисунок 2.4 Изменение размеров окна
1
Visual Prolog Integrated Development Engine – интегрированная среда разработки Visual Prolog.
10
2.2.
Включение панели задач: e.g. пункт File/New
Когда вы запускали пустое приложение, вы, скорее всего, заметили тот факт, что пункт меню File/New отключен. Чтобы включить его, дважды щелкните по ветке дерева проекта под названием TaskMenu.mnu (рис. 2.5). Затем, разверните дерево, находящееся в нижней части диалогового окна TaskMenu, и уберите галочку Disabled, относящуюся к пункту &New/tF7, как на рис. 2.6.
Рисунок 2.5 Панель задач приложения в дереве проекта
Рисунок 2.6 Включение элемента панели задач
11
2.3.
Добавление кода к предмету из дерева проекта в CodeExpert
Чтобы добавить код к пункту File/New, нажмите на ветке дерева проекта TaskWindow.win правой кнопкой мыши, которая откроет контекстное меню. Выберите пункт Code Expert (рис 2.7). Как на рис. 2.8, дважды щелкните на
Наконец, нажмите кнопку Add (ориентируйтесь по рис. 2.8), и дважды щелкните по ветке _ _
Рисунок 2.7 Переход в эксперт кода
Это откроет текстовый редактор, со следующим фрагментом: clauses onFileNew(_Source, _MenuTag).
Откомпилируйте программу, затем измените этот фрагмент так: clauses onFileNew(W, _MenuTag) :X := query::new(W), X:show().
Откомпилируйте программу снова, выбрав пункт Build/Build на панели задач, как на рис. 1.4. За‐ пустите программу, и вы увидите, что, когда вы нажимаете File/New, создается новая форма.
2.4.
Примеры
Пример в этой главе показывает, как создавать новую форму выбором пункта File/New, затем за‐ полнением диалогового окна Create Project Items.
12
Рисунок 2.8 Dialog and Window Expert
Рисунок 2.9 Появляющаяся форма
13
Глава 3: Mouse events В первых двух главах этого документа, вы научились компилировать приложение с формой в нём, которая появляется, когда вы выбираете пункт File/New из меню этого приложения.
3.1.
Добавим код к событию MouseDown
Нажмите на ветку дерева проекта query.frm правой кнопкой мыши, что откроет контекстное ме‐ ню. Выберите пункт Code Expert, как вы делали на рис. 2.8 для TaskWindow.win. Осторожно, на рис. 2.8, вы применяли эксперт кода к TaskWindow.win. Сейчас вам нужно применить его к query.frm. В code expert дважды щелкните на Mouse, затем на MouseDown. Наконец, нажмите кнопку Add, и дваж‐ ды щелкните на ветку
Вам следует вставить следующий код в процедуру onMouseDown/4: clauses onMouseDown(S, Point, _ShiftControlAlt, _Button):W=S:getVPIWindow(), Point=pnt(X,Y), Vpi::drawText(W, X, Y, “Hello, World!”).
Откомпилируйте программу, и запустите её. Выберите File/New чтобы создать новую форму. Ка‐ ждый раз, когда вы нажимаете на форму, программа размещает на ней знаменитое приветствие.
3.2.
onPaint
Вы использовали обработчик события onMouseDown/4 чтобы нарисовать что‐нибудь на форме. Есть программисты, которые не считают это хорошей идеей. Существуют языки, которые делают та‐ кой подход очень трудноосуществимым. Java один из таких языков, и очень популярный, между про‐ чим. В Java любое рисование следует производить внутри public abstract void doPaint(java.awt.Graphics g)
14
Конечно, это ограничение можно обойти, и даже в Java. В любом случае, давайте научимся про‐ изводить всё рисование внутри обработчиков событий onPaint/3. 1. Создайте проект с именем plot0. 2. Добавьте новый пакет plotter к нему, как ранее. 3. Вставьте новую форму query в plotter. 4. Включите пункт панели задач File/New 5. Добавьте фрагмент clauses onFileNew(S, _MenuTag) :Q=query::new(S), Q:show().
к ProjTree/TaskWindow.win/Menu/TaskMenu/id_file/id_file_new. Следующий шаг – добавить фрагмент class facts mousePoint:pnt := pnt(-1, -1). predicates onMouseDown : drawWindow::mouseDownListener. clauses onMouseDown(_S, Point, _ShiftControlAlt, _Button) :mousePoint := Point, Point=pnt(X,Y), R=rct(X-8, Y-8, X+60, Y+8), Invalidate(R).
в query/Code Expert/Mouse/MouseDown→onMouseDown. Вы готовы применять обработчик события onPaint к query. onPaint будет вызван каждый раз, когда прямоугольная область на форме стано‐ вится устаревшей или неверной. Когда это происходит, то есть, когда изображение становится не‐ верным? Когда окно частично или полностью перекрыто другим окном, или когда вы делаете его не‐ действительным с помощью предиката invalidate(R), где R – прямоугольник. Прямоугольник опи‐ сывается координатами его верхнего левого и нижнего правого углов. R=rct(X-8, Y-8, X+60, Y+8)
Вы заметили, что обработчик события onMouseDown/4 делает недействительным прямоуголь‐ ную область вокруг места щелчка мышью. Обработчик события onPaint будет работать с этой обла‐ стью. Под конец, добавьте фрагмент clauses onPaint(_Source, _Rectangle, GDIObject) :pnt(X, Y)=mousePoint, X>0, !, mousePoint:= pnt(-1, -1), GDIObject:drawText(X, Y, “Hello, World!”, -1). onPaint(_Source, _Rectangle, _GDIObject).
к query/Window/Paint→onPaint. Этот новый подход потребует от вас понимания множества но‐ вых понятий, таких, как отсечение (тот восклицательный знак, расположенный после X>0) и факты. Не беспокойтесь пока об этих понятиях. Вы все изучите в свое время.
3.3.
Примеры
Примеры в этой главе показывают, как обрабатывать события мыши, такие, как щелчок левой кнопкой мыши.
15
Глава 4: Поменьше картинок До того, как двигаться дальше, давайте найдем способ описывать наш проект, не опираясь по‐ стоянно на изображения.
4.1.
Панель задач
Панель задач, показанная на рис. 1.1 и 1.4, это главное меню Visual Prolog IDE. Запись A/B отно‐ сится к одному из её пунктов. Например, вы используете команду Project/New для создания нового проекта (рис. 1.1). Оно [диалоговое окно создания нового проекта – прим. пер.] имеет шесть форм, а именно: General, Directories, Build Options, Version Information, File Templates и Run Options. В большинстве слу‐ чаев, вам потребуется заполнять только форму General. General Project Name: plot2 UI Strategy: Object-oriented GUI (pfc/gui) Target Type: Exe Base Directory: E:\VIP7\Tyros
Когда вы найдете шаг, указывающий: Создайте новый GUI проект: plot2 (раздел 1.1.1). вам следует войти в диалоговое окно Project Settings (выбрав пункт Project/New на панели задач VDE1) и заполнить форму General как указано выше. Сделайте это mutatis mudandis2, так как на вашем компьютере может не быть диска E, или вы можете захотеть хранить ваши программы в папке, отли‐ чающейся от E:\VIP70\tyros.
4.2.
Дерево проекта
Самый лёгкий способ ориентироваться в файлах и ресурсах это щелкать мышкой по соответст‐ вующим элементам Дерева проекта:
Если вы дважды щёлкнете по папке, она откроется и отобразит свое содержимое. Если вы щёлк‐ нете по элементу правой кнопкой мыши, откроется контекстное меню, как на рисунке. Если я хочу, чтобы вы зашли в эксперт кода и добавили фрагмент кода в TaskWindow.win, я скажу: В эксперте кода, добавьте код к TaskWindow/TaskWindow.win (раздел 2.3) Чтобы выполнить это указание, отправляйтесь в дерево проекта и сделайте следующее: 1. Дважды щелкните по папке TaskWindow, чтобы открыть её, если она закрыта. 2. Щелкните правой кнопкой мыши по ветке дерева проекта TaskWindow.win, чтобы раскрыть контекстное меню, в котором будут следующие пункты:
1 2
Visual Development Environment – визуальная среда разработки лат. Изменив то, что следует изменить; внеся необходимые изменения.
16
Edit Attribute Delete Code Expert
3. Наконец, выберите пункт Code Expert
4.2.1. Code Expert Эксперт кода тоже имеет форму дерева.
Соответствуя названию, эксперт кода используется для вставки кода во многие файлы проекта. Чтобы добраться до эксперта кода, вы должны щелкнуть правой кнопкой мыши по элементу дерева проекта, к которому вы хотите добавить код. Затем, выберите пункт Code Expert из контекстного ме‐ ню. Чтобы перемещаться по дереву эксперта кода и добираться до точек, куда вы желаете вставить код, щелкайте по нужным веткам дерева. Если вы хотите, чтобы эксперт кода добавил прототип к «листку» дерева, нажмите на этот листок и затем нажмите на кнопку Add, которая появится внизу диалогового окна. Затем дважды щелкните по листку снова, чтобы перейти к коду. Если я попрошу: В Эксперте кода добавьте (раздел 2.3): clauses onFileNew(W, _MenuTag) :S=query::new(W), S:show().
к TaskWindow.win/Code Expert/Menu/TaskMenu/id_file/id_file_new, вот шаги, которым вы должны сле‐ довать: • Дерево проекта – Откройте папку TaskWindow дерева проекта, щелкните правой кноп‐ кой мыши по TaskWindow.win, чтобы открыть его контекстное меню, и выберите пункт Code Expert (рис. 2.7). • Эксперт кода – Дважды щелкните на Menu→дважды щелкните на TaskMenu→дважды щелкните на id_file. Выберите id_file_new и нажмите кнопку Add для создания кода прото‐
17
•
типа. Наконец, дважды щелкните на id_file_new→onFileNew. Ориентируйтесь по рис. 2.7, 2.8 и разделу 2.3. Файл: TaskWindow.pro – Добавьте требуемый код:
clauses onFileNew(W, _MenuTag) :S=query::new(W), S:show().
4.3.
Создание элемента проекта
Для добавления нового элемента к дереву проекта выберите команду File/New из панели задач. Следите за тем, чтобы размещать создаваемые элементы в нужных папках. Примеры можно найти на рис. 2.2 и 2.3. Вот как я попрошу вас создать пакет и добавить к нему форму: • Добавить новый пакет к дереву проекта: plot2/plotter. Внимание, если вы хотите, чтобы пакет располагался в корневом каталоге, не заполняйте поле parent directory, как на рис. 4.1. Один из способов начать с пустого родительского каталога – выбрать корень дерева проекта до того, как нажимать File/New, как показано на рис. 2.1.
Рисунок 4.1 Создание кового пакета
•
Создать новую форму: plotter/query (раздел 2.1). Создайте форму: query. Поместите query в пакет plotter. Чтобы поместить окно внутрь пакета, убедитесь, что выбрана папка пакета, до того, как отправляться в File/New. Выбирайте имена, которые что‐то значат. Например, если пакет хранит диаграммы, назовите его plots1; если он хранит компьютер‐ ную графику, вы можете выбрать имя вроде canvasFolder; если он содержит запросы, хо‐ рошим именем будет queryForms; если он содержит формы для заполнения, это portfolio; и т. д. Когда вы создаете форму, IDE показывает диалоговое окно Form Attributes, которое вы можете принять без изменений. После диалогового окна Form Attributes, IDE покажет диалоговое окно Form Window (см. рис. 4.2). Вы можете воспользоваться возможностью изменить размеры окна и добавить строку ввода (название: edit_ctl) и кнопку, как показано на рисунке. Вы также можете добраться до диалогового окна Form Window, нажав правой кнопкой мыши по имени формы в дереве проекта, и выбрав пункт Edit из появившегося меню.
1
англ. График, чертёж, диаграмма.
18
Рисунокк 4.2 Форма со о строкой ввод да
4.4 4.
Панель зад дач.
Если и вы дважды щёлкнетее по ProjectT Tree/TaskMeenu.mnu, вы получите д диалоговое о окно вроде то‐ го, которое было по оказано на рис. 2.6. Вы ы можете раазвернуть деерево этой сспецификац ции меню щ щел‐ кая по еего веткам, ккак говорил лось ранее. Вам также м может понадобиться вкключить каккой‐либо пункт меню, ккак в раздел ле 2.2. Такжее возможно о создать новый элемен нт меню, какк показано н на рис. 4.2, где я нажал л на кнопку ((иконку) под д названием м New Item и создал пункт Drawingg Tools, запо олнив диало ого‐ вое окно. Как вы м можете увид деть из рисуунка, новый пункт созд дается изначчально вклю юченным. Си им‐ вол & в записи &Filee обозначаеет F как горяячую клавиш шу.
4.5 5.
Сы ыграем сснова.
Даввайте начнем м новый проект с нуля,, чтобы посм мотреть, что о мы сможеем сделать сс меньшим ко‐ личеством картинок и провери ить, сжато ли и мы описываем проектт Visual Prolog. те новый G GUI проект:: factorial l (раздел 1.1.1). • Создайт
1 19
• • • •
Добавьте новый пакет к дереву проекта: factorial/forms. Создайте новую форму: forms/query (раздел 4.3). Добавьте edit field [строку ввода] (с названием edit_ctl) и кнопку (с названием factorial_ctl) на неё, как на рис. 4.2. Включите пункт TaskMenu File/New (раздел 2.2). Откомпилируйте приложение, затем добавьте (раздел 2.3):
clauses onFileNew(W, _MenuTag) :S=query::new(W), S:show().
к TaskWindow.win/Menu/TaskMenu/id_file→id_file_new→onFileNew.
4.6.
Создание нового класса
Чтобы создать новый класс и вложить его в пакет forms, выберите папку forms в дереве проекта и выберите пункт File/New в панели задач VDE. Убедитесь, что убрали галочку Create Objects, как пока‐ зано:
Когда вы нажмёте кнопку Create, Visual Prolog предоставит нам файлы fn.pro и fn.cl, которые со‐ держат прототип класса fn. Наша задача – добавить функциональности к этим файлам. Начнем с fn.cl: class fn open core predicates classInfo : core::classInfo. setVal : (string) procedure. calculate : () procedure. end class fn
На языке объектно‐ориентированного программирования, файл fn.cl содержит интерфейс ме‐ тодов setVal:(string) procedure и calculate:(). Он информирует предполагаемых пользо‐ вателей о том, что метод setVal/1 это процедура (об этом позже), и требует один аргумент, кото‐ рый должен быть строкой; он также говорит, что метод calculate/0 не имеет аргументов. С другой стороны, файл fn.pro содержит определение обеих процедур. %файл fn.pro implement fn open core
20
constants className = “forms/fn”. classVersion = “”. class facts nVal:integer := 0. class predicates fact:(integer, integer) procedure (i,o). clauses classInfo(className, classVersion). setVal(X) :nVal:=toterm(X), stdio::write(“fact(“, nVal, “)=”). fact(0,1) :- !. fact(N, N*F) :fact(N-1, F). calculate() :fact(nVal, F), stdio::write(F, “\n”). end implement fn
Следующий шаг – вызвать предикат setVal/1, когда пользователь введёт что‐нибудь в строку ввода. Отправляйтесь в дерево проекта и примените эксперт кода к query.frm. Если вы не помните, как это делать, прочтите раздел 3.1 заново. В эксперте кода откройте папку Control/edit_ctl (дважды щелкнув на ней), щелкните на LoseFocus, и нажмите кнопку Add, чтобы создать прототип кода. Дваж‐ ды щелкните на LoseFocus→onEditLoseFocus, и добавьте следующий фрагмент кода: predicates onEditLoseFocus : window::loseFocusListener. clauses onEditLoseFocus(S) :fn::setVal(S:getText()).
Когда учишь язык программирования, требуется время, чтобы привыкнуть к новым понятиям. Поэтому, не пытайтесь, на данный момент, понять каждую деталь программ, которые вы найдёте в этом руководстве. Согласно Эзопу, осведомленность прояснит вещи позже. В любом случае, давайте вкратце пройдемся по вещам, которые нужно запомнить. В Visual Prolog есть классы и объекты, принадлежащие классам. Объект имеет набор методов, которые являются программами, которые работают с объектом. Они также называются event driven predicates – движимые событиями предикаты. Если вы поставите курсор в строку ввода, она получит фокус. Строка ввода потеряет фокус, если вы обратите свое внимание на другой объект, например, кнопку. Фрагмент clauses onEditLoseFocus(S) :fn::setVal(S:getText()).
гласит, что, если строка ввода когда‐нибудь потеряет фокус, она должна вызвать метод S:getText(), чтобы получить строку, которую ввел пользователь, и передать её в fn::setVal(X), где она будет сокращена до целого числа: setVal(X) :nVal:=toterm(X).
21
setVal/1 сохранит целое число в факте‐переменной nVal. Следующий шаг – снова применить эксперт кода к query.frm. Отправляйтесь в дерево проекта и добавьте фрагмент clauses onFactorial(_Source) = button::defaultAction() :fn::calculate().
к query.frm/Code Expert/Control/factorial_ctl→onFactorial. Наконец, откомпилируйте проект и за‐ пустите программу. Если вы не помните как компилировать и запускать программу, взгляните на раздел 1.1.2. В новом приложении выберите пункт File/New. Выскочит новая форма. Наберите число в строке ввода и нажмите на кнопку factorial. Класс fn напишет факториал в окне сообщений.
4.7.
Содержимое строки ввода
Получить содержимое строки ввода довольно мудрено. Это верно для Visual C, Delphi, OCAML, Clean, и пр. Оценивая Visual Prolog, я должен сказать, что он предлагает самый легкий доступ к эле‐ ментам управления, по сравнению с остальными языками. Хотя он все равно мудреный. Вы изучили способ получить доступ к содержимому строки ввода: Взять его окно, когда оно по‐ теряет фокус, затем использовать это окно для доступа к его содержимому, которое вы можете ис‐ пользовать для дальнейших вычислений. Вы будете рады узнать, что Visual Prolog хранит окно в фак‐ те‐переменной для вас. Следовательно, вам не требуется заигрывать с фокусом. • Создайте новый GUI проект: fact2 (раздел 1.1.1). • Добавьте новый пакет к дереву проекта: fact2/forms (раздел 4.3). • Создайте новую форму: forms/query (раздел 4.3). Добавьте на неё строку ввода (с назва‐ нием edit_ctl) и кнопку (с названием factorial_ctl), как на рис. 4.2. • Включите пункт панели задач File/New (раздел 2.2). Откомпилируйте программу, что‐ бы включить форму query в проект. • В эксперте кода добавьте (раздел 2.3): clauses onFileNew(W, _MenuTag) :S=query::new(W), S:show().
к TaskWindow.win/Menu/TaskMenu/id_file→id_file_new→onFileNew. Добавьте фрагмент clauses onFactorial(_S)=button::defaultAtcion() :-
22
fn::calculate(edit_ctl:getText()).
к query.frm/Code Expert/Control/factorial_ctl→onFactorial. Создайте класс fn внутри forms. Уберите галочку Creates Objects. Отредактируйте файлы fn.cl и fn.pro как показано ниже. %File: fn.cl class fn open core predicates classInfo : core::classInfo. calculate:(string) procedure. end class fn % File fn.pro implement fn open core constants className = "forms/fn". classVersion = "". class predicates fact:(integer, integer) procedure (i,o). clauses classInfo(className, classVersion). fact(0, 1) :- !. fact(N, N*F) :- fact(N-1, F). calculate(X) :- N= toterm(X), fact(N, F), stdio::write(F, "\n"). end implement fn
Откомпилируйте и запустите программу, как и ранее. В этой новой версии программы, текст из‐ влекается из строки ввода использованием метода edit_ctl:getText(), что гораздо проще, чем ранее.
4.8.
Примеры
В этой главе три примера: plot2 показывает, как создавать форму с кнопками и полями; factorial вычисляет факториал; fact2 тоже вычисляет факториал.
23
Глава 5: Предложения Хорна 5.1.
Функции
Я уверен, что вы знаете, что такое функция. Возможно, вы не знаете математического определе‐ ния функции, но вы имеете представление о функциях, полученное использованием калькуляторов и компьютерных программ или посещением курсов элементарной алгебры. Функция имеет функтор, который является именем функции, и аргументы. Например: sin(X) – функция. Другой пример функ‐ ции – mod(X, Y), которая возвращает остаток от деления. Когда вы хотите использовать функцию, вы подставляете постоянные значения вместо переменных, или аргументов. Например, если вы хотите найти остаток от 13, делённого на 2, вы можете набрать mod(13, 2) на вашем калькуляторе (если он имеет эту функцию, конечно же). Если вы хотите синус 30°, вы можете набрать sin(30). Можно сказать, что функции это отображения возможных значений аргумента в значение из множества результатов вычисления. Доменом называется множество возможных значений аргумен‐ та. Множество результатов вычисления называется образом. В случае функции синуса, элементами домена являются действительные числа между 0 и 2π радиан (или 0° и 360°)1. Есть одна вещь, кото‐ рую следует запомнить. Математики настаивают, что функция должна возвращать только одно зна‐ чение для каждого заданного аргумента. Таким образом, если вычисление производит большее ко‐ личество значений, оно не является функцией. Например, 4 может быть 2 или ‐22. Тогда, квадрат‐ ный корень числа не является функцией3. Что насчет функций нескольких аргументов? Например, функция max(X, Y) принимает два аргу‐ мента, и возвращает наибольший из них. В этом случае, вы можете считать, что она имеет только один аргумент, который является парой значений. Так, аргументом max(5, 2) является пара (5, 2). Ма‐ тематики говорят, что домен подобной функции – декартово произведение R×R. Есть функции, чей функтор располагается между аргументами. Это применимо в случае арифме‐ тических операторов, где пишут 5+7 вместо +(5, 7).
5.2.
Предикаты
Предикаты это функции, чьим образом является множество {verum, falsum}, или, если вам не нравятся латинские названия, используемые в логике, вы всегда можете полагаться на английский эквивалент: {true, false}. Есть несколько предикатов, известных любому, кто приложил руку к программированию, или даже студенту, посещающему лекции элементарной алгебры. Вот они: X>Y возвращает true если X больше, чем Y, иначе возвращает false. X
Предикат с одним аргументом говорит о свойстве или особенности его аргумента. Можно ска‐ зать, что такой предикат работает, как прилагательное. В C, ~X true, если X false, иначе ~X false. В других языках программирования существуют предикаты, эквивалентные этому. Ещё примеры од‐ номестных предикатов: positive(X): true если X положительный, false в любом другом случае. exists(“text.txt”): true если файл text.txt существует, иначе false.
1
Функция Y = sin(X) определена на всем множестве действительных чисел. Автор указал промежуток области определения, длина которого равна периоду этой функции. 2 Под выражением √4 обычно понимается арифметический квадратный корень, т.е. корень из 4 равен 2, а 4. не 2 или ‐2, как написано в тексте. Автор понимает выражение √4 как решение уравнения 3 С учётом предыдущего примечания – является.
24
Предикат более чем с одним аргументом показывает, что между его аргументами существует от‐ ношение. В случае X=Y это отношение – равенство. Было бы интересно иметь язык программирования с предикатами, устанавливающими свойства и отношения иные, чем те несколько, предлагаемые основными компьютерными языками и кальку‐ ляторами. Например, более одного человека, за его или её жизнь, чувствовали срочную необходи‐ мость в одном из предикатов, перечисленных на рис. 5.1, особенно в третьем. Пролог является язы‐ ком программирования, изобретенным для удовлетворения этой потребности.
positive(X) true если X положителен, иначе false rain(Temperature, Pressure, Humidity) true, если будет
дождь при заданных температуре, давлении и влажности. Например, rain(100, 1.2, 90)
вернет true, если пойдет дождь, когда ваши измерительные при‐ боры покажут 100°F, 1.2 атмосфер, и 90% относительной влажности. invest(Rate, StandardDeviation, Risk) По заданным Rate, StandardDeviation, и приемлемом Risk, этот предикат возвращает true, если вам стоит выбрать инвестирование. Рисунок 5.1 Интересные предикаты.
5.3.
Решения
Предположим, что у вас есть предикат city(Name, Point), который определяет координаты города на карте. Предикат city/2 имеет домен1 city : (string Name, pnt Position).
и может быть определен как база данных фактов: city(“Salt Lake”, pnt(30, 40)). city(“Logan”, pnt(100, 120)). city(“Provo”, pnt(100, 200)). city(“Yellowstone”, pnt(200, 100)).
Этот предикат проверяет, является ли заданное положение заданного города верным, когда кто‐ либо не уверен насчет этого. Вот несколько запросов, которые можно задать предикату city/2. city("Salt Lake", pnt(30, 40)) → true city("Logan", pnt(100, 200)) → false city("Provo", pnt(100, 200)) → true
Нет вопросов, которым вы можете найти применение для подобного предиката. Хотя, предикат, возвращающий координаты города по заданному названию, может оказаться намного более полез‐ ным. city(“Salt Lake”, P) → P=pnt(30, 40).
В этом новом виде предикатов, символы1, начинающиеся с заглавной буквы, называются пере‐ менными. Примеры переменных: X, Y, Wh, Who, B, A, Xs, Temperature, Humidity, Rate. Таким образом,
1
N.B. Предикатами являются функции, чьим доменом может быть любое декартово произведение, но чьим образом может быть только множество {true, false}. – прим. авт.
25
если вы хотите проверить, является ли символ переменной, проверьте его первую букву. Если она заглавная или даже является знаком подчеркивания ( _ ), вы имеете дело с переменной. Когда вы используете переменную, как P в запросе city(“Salt Lake”, P), вы желаете знать, что нужно подставить вместо P, чтобы сделать предикат city(“Salt Lake”, P) true, то есть ис‐ тинным. Ответом является P=pnt(30, 40). Софокл сказал, что руки не должны быть быстрее ума, но и не должны отставать от него. Таким образом, давайте определим предикат city/2. • Project Settings. Войдите в диалоговое окно Project Settings, выбрав пункт Project/New из панели задач VDE, и заполните его. General Project Name: mapDataBase UI Strategy: Object-oriented (pfc/gui) Base Directory: C:\VIP7 Target Type: Exe Sub-Directory: mapDataBase\
•
Create Project Item: Package. Выберите узел дерева проекта mapDataBase. Выберите пункт меню File/New. В диалоговом окне Create Project Item, выберите package и заполните по‐ ля:
Name: plotter Parent Directory :
•
NB: Пустое поле Parent Directory присоединяет пакет к корневому каталогу. Create Project Item: Form. Выберите узел дерева проекта plotter. Выберите File/New. В диалоговом окне Create Project Item выберите form. Заполните диалоговое окно:
Name: map Package: plotter.pack (plotter\)
• • • •
Добавьте следующие кнопки к новой форме: logan_ctl, saltLake_ctl, provo_ctl. Window Edit. Измените размеры новой формы. Окно формы должно иметь достаточный размер, чтобы отобразить нашу «карту». Оставьте побольше пустого места в центре фор‐ мы. Откомпилируйте проект. Это важно: в панели задач, Build/Build программу, иначе она выдаст сообщение об ошибке на следующем шаге2. Project Tree/TaskMenu.mnu. Включите File/New. Project Tree/TaskWindow.win/Code Expert. Добавьте
clauses onFileNew(S, _MenuTag) :X=map::new(S), X:show().
•
к Menu/TaskMenu/id_file/id_file_new/onFileNew. Создайте класс. Создайте класс draw, как объяснялось в разделе 4.6. Чтобы создать но‐ вый класс и вложить его в пакет plotter, выберите папку plotter в дереве проекта и выбе‐ рите пункт File/New из панели задач VDE. Название класса draw, и галочка Create Objects снята. Откомпилируйте проект, для того, чтобы внести прототип нового класса в дерево проекта. Затем отредактируйте draw.cl и draw.pro как показано на рис. 5.2 и 5.3.
1
Под словом «символ» на самом деле понимается любая последовательность символов. Я думаю, это основано на типе данных symbol, используемом в языке Пролог. 2 Вообще, можно это и не делать. Просто в процессе сборки программы компилятор поэтапно вкладывает в проект необходимые модули. Вы можете выполнить все указания автора, не компилируя программу до самого момента запуска, но потом вам просто придётся несколько раз подряд нажать Build/Build, до тех пор, пока компилятор не прекратит ругаться на необъявленные классы и модули (при условии, что вы сделали всё правильно, и он ругается не из‐за действительных ошибок в вашем проекте).
26
Для того, чтобы вызывать предикат drawThem кнопками городов, отправляйтесь в дерево проек‐ та и добавьте следующий фрагмент кода clauses onLogan(S) =button::defaultAction() :Parent=S:getParent(), P=Parent:getVPIWindow(), draw::drawThem(P, “Logan”).
к map.frm/Code Expert/Contro/logan_ctl/onLogan.
% File: draw.cl class draw open core, vpiDomains predicates classInfo : core::classInfo. drawThem:(windowHandle, string) procedure. end class draw
Рисунок 5.2 mapDataBase/plotter/draw.cl
% File:draw.pro implement draw open core, vpiDomains, vpi constants className = "plotter/draw". classVersion = "". class facts city:(string, pnt). clauses classInfo(className, classVersion). city("Salt Lake", pnt(30, 40)). city("Logan", pnt(100, 120)). city("Provo", pnt(100, 80)). city("Yellowstone", pnt(200, 100)). drawThem(Win, Name) :B= brush(pat_solid, color_red), winSetBrush(Win, B), city(Name, P), !, P= pnt(X1, Y1), X2= X1+20, Y2= Y1+20, drawEllipse(Win, rct(X1, Y1, X2, Y2)). drawThem(_Win, _Name). end implement draw
Рисунок 5.3 mapDataBase/plotter/draw.pro
27
Повторите действия для “Salt Lake” и “Provo”. Не забудьте заменить названия предложений1 на onSaltLake(S) и onProvo(S) соответственно, а также названия городов в drawThem. Откомпили‐ руйте проект и запустите программу. Если вы не помните, как компилировать и запускать программу, взгляните на раздел 1.1.2. В новом приложении, выберите пункт File/New. Появится новая форма. Когда вы нажимаете на кнопку, программа нарисует соответствующий город.
5.4.
Множественные решения
В предыдущем разделе вы видели предикаты, которые использовали свои переменные для по‐ лучения решения, а не для проверки истинно отношение или ложно. В примере, предикат city/2 использован для получения только одного решения. Тем не менее, существуют ситуации, требующие более одного решения. Пусть conn/2 будет предикатом, устанавливающим связь между двумя горо‐ дами. conn(pnt(30, 40), pnt(100, 120)). conn(pnt(100, 120), pnt(100, 200)). conn(pnt(30, 40), pnt(200, 100)). . . .
Вы можете использовать его для получения всех соединений между городами, как показано в примере: conn(pnt(30, 40), W). conn(X, Y).
→ → → → →
W=pnt(100, 120) W=pnt(200, 100) X=pnt(30, 40) / Y=pnt(100, 120) X=pnt(100, 120) / Y=pnt(100, 200) X=pnt(30, 40) / Y=pnt(200, 100)
Рассмотрим запрос: conn(pnt(30, 40), W)? Ответом может быть W=pnt(100, 120), но им также может быть W=pnt(200, 100).
5.4.1. Программа, использующая предикаты с множественными решениями Давайте создадим программу, чтобы показать всю прелесть возможности множественных реше‐ ний в Прологе – возможности, отсутствующей в других языках. • Project Settings. Зайдите в диалоговое окно Project Settings, выбрав Project/New на панели задач, и заполните его. Project Name: drawMap UI Strategy: Object-oriented GUI (pfc/gui) Target type: Exe Base Directory: C:\vip70
•
Create Project Item: Package. Выберите узел дерева проекта drawMap. Выберите пункт File/New на панели задач. В диалоговом окне Create Project Item выберите package и за‐ полните поля:
Name: plotter Parent Directory:
1
clause (англ.) – грам. предложение (являющееся частью сложного предложения).
28
•
Create Project Item: Window. Выберите узел дерева проекта plotter. Выберите пункт File/New на панели задач. В диалоговом окне Create Project Item выберите пункт Form. За‐ полните поля:
Name: map Package: plotter.pack (plotter\)
% File: draw.cl class draw open core, vpiDomains predicates drawThem:(windowHandle) procedure. end class draw % File: draw.pro implement draw open core, vpiDomains, vpi class facts city:(string Name, pnt Position). conn:(pnt, pnt). class predicates connections:( windowHandle). drawCities:(windowHandle). clauses city("Salt Lake", pnt(30, 40)). city("Logan", pnt(100, 120)). city("Provo", pnt(100, 160)). city("Yellowstone", pnt(200, 100)). conn(pnt(30, 40) , pnt(100, 120)). conn(pnt(100, 120), pnt(100, 160)). conn(pnt(30, 40), pnt(200, 100)). drawCities(W) :- city(N, P), P= pnt(X1, Y1), X2= X1+10, Y2= Y1+10, drawEllipse(W, rct(X1, Y1, X2, Y2)), drawText(W, X1, Y1, N), fail. drawCities(_Win). connections(Win) :- conn(P1, P2), drawLine(Win, P1, P2), fail. connections(_Win). drawThem(Win) :- connections(Win), drawCities(Win). end implement draw
Рисунок 5.4 draw.cl and draw.pro
• • • •
Form Properties. Не добавляйте TaskMenu к новой форме. Form Edit. Измените размеры новой формы. Project Tree/TaskMenu.mnu. Включите File/New. Project Tree/TaskWindow.win/Code Expert. Добавьте
clauses onFileNew(S, _MenuTag) : F= map::new(S), F:show().
29
•
•
к Menu/TaskMenu/id_file/id_file_new/onFileNew. Создайте класс. Создайте класс draw, как объяснено в разделе 4.6. Уберите галочку Create Objects. Внесите код для класса в файлы draw.cl и draw.pro как показано на рис. 5.4. Project Tree/map.frm/Code Expert. Примените эксперт кода к форме map.frm. В дереве эксперта кода откройте папку Window и вставьте следующий код в Paint→onPaint:
clauses onPaint(S, _Rectangle, _GDIObject) :W=S:getVPIWindow(), draw::drawThem(W).
Если вы откомпилируете программу, вы должны получить окно с картой на нём, наподобие рис. 5.5, когда нажмёте File/New.
Рисунок 5.5 Города штата Юта
5.5.
Логические связки.
Я полагаю, вы уже знакомы с логическим И, которое присутствовало в языках вроде Pascal или C: if ((X>2) && (X<4)) {…}
если P1 и P2 предикаты, P1 И P2 равно true если и P1 и P2 равны true. Последовательность предика‐ тов, связанных логическими И называется конъюнкцией. В языке C, (X>2) && (X<4)
является конъюнкцией. В Прологе предикаты конъюнкции разделяются запятыми. Тогда, выражение (X>2) && (X<4) превращается в X>2, X<4
Логическое И называется связкой.
5.6.
Импликация
Импликация это связка, изображаемая символом :-, который означает если. Таким образом, drawThem(Win):- connections(Win), drawCities(Win).
30
означает, что вы drawThem на Win, если вы нарисуете connections на Win и drawCities на Win.
5.7.
Предложения Хорна.
Предложением Хорна может быть единичный предикат. Например, в нижеследующем списке находятся четыре однопредикатных предложения Хорна. city("Salt Lake", pnt(30, 40)). city("Logan", pnt(100, 120)). city("Provo", pnt(100, 200)). city("Yellowstone", pnt(200, 100)).
Однопредикатные предложения Хорна называются фактами. В этом примере факты устанавли‐ вают отношение между городом и его координатами. Доменом этих предикатов является набор пар, состоящих из названия города и его координат. Предложение Хорна может также иметь вид H:-T1, T2, T3…
где Ti и H – предикаты. Так, drawThem(Win) :connections(Win), drawCities(Win).
является примером предложения Хорна. В предложении Хорна часть, расположенная до знака :-, называется head – голова (заголовок). Часть, расположенная после знака :- называется tail – хвост. В примере головой является drawThem(Win), а хвостом – connections(Win), drawCities(Win). Набор предложений Хорна с одинаковым заголовком определяет предикат. Например, четыре предложения Хорна о городах определяют предикат city/2, и drawThem(Win) :- connections(Win), drawCities(Win).
определяют drawThem/1.
5.8.
Объявления
Определение предиката не полно без объявления типов и flow pattern – шаблона потока. Вот объявление типов и потока (режима) drawThem/1. predicates drawThem : (windowHandle) procedure (i).
Объявление типа гласит, что аргумент Win предиката drawThem(Win) имеет тип windowHandle. Объявление режима утверждает, что аргумент drawThem/1 обязан быть входным, то есть, он являет‐ ся константой, не свободной переменной. В этом случае, предикат принимает данные через свой ар‐ гумент. Вообще, вам не нужно предоставлять объявление режима. Компилятор самостоятельно сделает выводы о режимах предиката. Если он не сможет это сделать, он выдаст сообщение об ошибке и вы сможете добавить объявление режима сами. Если предикат предполагается использовать только внутри его класса, он объявляется как class predicate – предикат класса. Примеры: class predicates connections : ( windowHandle ). drawCities : (windowHandle ).
Однопредикатные предложения Хорна могут быть объявлены как факты. Например:
31
class facts city : (string Name, pnt Position). conn : (pnt, pnt).
Позже, вы увидите, что факты могут быть добавлены (asserted) или удалены (retracted) из базы данных. Аргументы conn/2 – пара значений типа pnt. Тип pnt определен в классе vpiDomains. Что‐ бы использовать его в классе draw, у меня есть две возможности. Я могу явно назвать класс, к кото‐ рому принадлежит pnt: class facts conn : (vpiDomains::pnt, vpi::Domains::pnt).
или иначе, я могу открыть этот класс внутри класса draw. Я выбрал второй вариант: open core, vpiDomains, vpi
5.8.1. Объявления режимов детерминизма Чтобы объявить, имеет предикат одно решение или несколько, используются следующие ключе‐ вые слова: determ determ предикат может завершиться неудачно (fail), или успешно (succeed), с одним решением.
procedure Этот вид предикатов всегда завершается успешно, и имеет одно решение. Предикаты connections/1 и drawCities/1, определенные на рис. 5.4, процедуры, и могут быть объявлены так: class predicates connections:(windowHandle) procedure (i). drawCities:(windowHandle) procedure (i).
multi multi предикат не может завершиться неудачно и имеет множество решений.
nondeterm nondeterm предикат может завершиться неудачно или успешно многими способами. Факты city/2 и connection/2 оба nondeterm, и принимают следующее объявление: class facts city:(string Name, pnt Position) nondeterm. conn:(pnt, pnt) nondeterm.
Если у предиката есть множество решений, и одно из его решений не удовлетворяет предикату в конъюнкции, Пролог делает backtrack – откат, и предлагает другое решение в попытке удовлетво‐ рить конъюнкции. Взгляните на предложение Хорна: connections(Win) :- conn(P1, P2), drawLine(Win, P1, P2), fail. connections(_Win).
nondeterm факт conn(P1, P2) предоставляет две точки, которые используются процедурой drawLine(Win, P1, P2) для рисования прямой линии. Затем, Пролог пытается удовлетворить предикату fail, который всегда заканчивается неудачно, соответствуя своему названию. Следова‐
32
тельно, Пролог делает откат и пробует другое решение, пока он не исчерпает все возможности, и не попробует второе предложение connection/1, которое всегда успешно.
5.9.
Предикаты рисования
Мой тринадцатилетний сын думает, что единственное использование компьютерных языков это игры, графика и тому подобное. Если вы придерживаетесь того же мнения, вам необходимо глубоко изучить предикаты рисования. Они требуют дескриптор (handle1) окна, которое будет поддерживать рисунки и чертежи. Вот как вы можете получить дескриптор: clauses onPaint(S, _Rectangle, _GDIObject) : W= S:getVPIWindow(), draw::drawThem(W).
Внутри класса draw будет передаваться дескриптор W. Например, в предложении drawThem(Win) :- connections(Win), drawCities(Win).
он передается в connections/1, где он является первым аргументом drawLine/3: connections(Win) :- conn(P1, P2), drawLine(Win, P1, P2), fail.
Предикат drawLine(Win, P1, P1) чертит линию из P1 в P2 на окне Win. Как вы знаете, P1 и P2 – точки, как pnt(10, 20). Иной предикат, с которым вы знакомы, это drawEllipse(W, rct(X1, Y1, X2, Y2))
который чертит эллипс на окне W. Эллипс вписан в прямоугольник rxt(X1, Y1, X2, Y2), где X1, Y1 – координаты верхнего левого угла, и X2, Y2 – координаты нижнего правого угла.
5.10.
GDI объект
В последнем примере применялось рисование из обработчика события onPaint. Когда это про‐ исходит, может быть хорошей идеей использование методов из так называемого GDI объекта. Сле‐ дующий пример показывает, как это работает. • Project Settings. Создайте следующий проект: Project Name: drawMapObj UI Strategy: Object-oriented GUI (pfc/gui) Target type: Exe Base Directory: C:\vip70
• • •
•
Создайте пакет: plotter. Создайте форму внутри plotter: map. Project Tree/TaskMenu.mnu. Включите File/New. Из панели задач, Build/ Build приложение, для включения формы map в проект. Я надеюсь, что в один прекрасный день в отдалён‐ ном будущем, мы будем избавлены от этого шага. Но сейчас, не забывайте это. Project Tree/TaskWindow.win/Code Expert. Добавьте
clauses onFileNew(S, _MenuTag) : F= map::new(S), F:show().
к Menu/TaskMenu/id_file/id_file_new/onFileNew Создайте класс. Создайте класс draw внутри пакета plotter. Не забудьте отключить “Create ob jects”. Вы найдете новую версию класса draw на рис. 5.6. Откомпилируйте приложение. 1 На данный момент вам не обязательно знать, что такое handle. – прим. авт.
33
•
Добавьте
clauses onPaint(_S, _Rectangle, GDIObject) : draw::drawThem(GDIObject).
к Project Tree/map.frm/Code Expert/Window/Paint.
5.11.
Примеры
В этой главе три примера: mapDataBase рисует положение трёх главных городов штата Юта, ко‐ гда нажимается соответствующая кнопка; drawMap рисует карту направлений дорог Юты; drawMa pObj является GDI версией drawMap.
%File: draw.cl class draw open core predicates drawThem:(windowGDI). end class draw % File: draw.pro implement draw open core, vpiDomains, vpi class facts city:(string Name, pnt Position). conn:(pnt, pnt). class predicates connections:( windowGDI). drawCities:(windowGDI). clauses city("Salt Lake", pnt(30, 40)). city("Logan", pnt(100, 120)). city("Provo", pnt(100, 160)). conn(pnt(30, 40) , pnt(100, 120)). conn(pnt(100, 120), pnt(100, 160)). drawCities(W) :- city(N, P), P= pnt(X1, Y1), X2= X1+10, Y2= Y1+10, W:drawEllipse(rct(X1, Y1, X2, Y2)), W:drawText(pnt(X1, Y1), N), fail. drawCities(_Win). connections(W) :- conn(P1, P2), W:drawLine(P1, P2), fail. connections(_W). drawThem(Win) :- connections(Win), drawCities(Win). end implement draw
Рисунок 5.6 Класс draw.
34
Глава 6: Консольные приложения Так как графические приложения неплохо смотрятся, мы начали с них. Хотя, они добавляют де‐ тали, которые отклоняют ваше внимание от действительно важного. Поэтому, давайте рассмотрим несколько базовых схем программирования, не рассчитывая на графический интерфейс.
6.1.
Отсечение
Что вам нужно сделать, если вы хотите, чтобы система ни делала откат, ни пыталась найти другое предложение после нахождения решения? В этом случае, вы ставите восклицательный знак в хвосте предложения Хорна. После того, как система найдет восклицательный знак, она прерывает поиск новых решений. Восклицательный знак называется cut – отсечение. Давайте проиллюстрируем ис‐ пользование отсечений очень популярным алгоритмом среди программистов на Прологе. Предпо‐ ложим, что вы хотите написать предикат, который находит факториал числа. Математики определя‐ ют факториал как: factorial(0)→1 factorial(n)→n×factorial(n-1)
Используя хорновские предложения, это определение становится следующим: fact(N, 1) :- N<1, !. fact(N, N*F1) :- fact(N-1, F1).
Отсечение предотвращает попытку Пролога применить второе предложение для N=0. Другими словами, если вы поставите запрос fact(0, F)?
программа успешно использует первое предложение для N=0, и ответит, что F=1. Без отсечения она могла бы подумать, что второе предложение определения fact(N, 1) :- N<1, !. fact(N, N*F1) :- fact(N-1, F1).
тоже даст решение. Тогда, она попробовала бы использовать его, и сбилась бы. Отсечение обес‐ печивает, что факториал – функция и отображает каждое значение из домена в одно и только одно значение множества образа. Чтобы реализовать функцию факториала, следуйте следующим указа‐ ниям: • Создание нового проекта. Выберите пункт Project/New и заполните диалоговое окно Project Settings так: General Project Name: facfun UI Strategy: console
•
Обратите внимание, что мы собираемся использовать консольную стратегию, не GUI. Сборка. Выберите пункт Build/Build из панели задач, чтобы внести прототип класса facfun в дерево проекта. Отредактируйте facfun.pro, как показано ниже.
implement facfun open core, console class predicates fact : (integer, integer) procedure (i,o). clauses classInfo(“facfun”, “1.0”).
35
fact(N, 1) :- N<1, !. fact(N, N*F1) :- fact(N-1, F1). run() :- console::init(), fact(read(), F), write(F), nl. end implement facfun goal mainExe::run(facfun::run).
Снова откомпилируйте программу, и запустите её, используя команду Run in Window. Напишите число в приглашении к вводу, и вы получите его факториал. NB: Чтобы протестировать консольную программу, используйте Run in Window, не Execute.
6.2.
Списки
Есть книга о языке Lisp, которая гласит: список это упорядоченная последовательность элемен‐ тов, где упорядоченная означает, что порядок имеет значение. В Прологе список помещается между квадратными скобками, и подразумевается имеющим голову (head) и хвост (tail): Список [3, 4, 5, 6, 7] [“wo3”, “ni3”, “ta1”] [4] [3.4, 5.6, 2.3]
Тип Integer* String* Integer* Real*
Голова 3 “wo3” 4 3.4
Хвост [4, 5, 6, 7] [“ni3”, “ta1”] [] [5.6, 2.3]
Вы можете соотнести шаблон переменных со списком. Например, если вы соотнесете шаблон [X|Xs]
со списком [3.4, 5.6, 2.3], вы получите X=3.4 и Xs=[5.6, 2.3], то есть, X соответствует голове списка и Xs соответствует хвосту. Конечно же, вы можете использовать другие две переменные вместо X и Xs в шаблоне [X|Xs]. Так, [A|B], [X|L], [Head|Tail], [First|Rest] и [P|Q] – эквивалентные шаблоны. Ещё примеры соответствий для шаблона [X|Xs]: Шаблон [X|Xs] [X|Xs] [X|Xs] [X|Xs]
Список [3.4, 5.6, 2.3] [5.6, 2.3] [2.3] []
X 3.4 5.6 2.3
Xs [5.6, 2.3] [2.3] [] Не соответствуют
Как вы можете видеть, шаблон [X|Xs] соответствует только списку с как минимум одним элемен‐ том. Вот шаблон, который соответствует только спискам с как минимум двумя элементами: Шаблон [X1, X2|Xs] [X1, X2|Xs] [X1, X2|Xs] [X1, X2|Xs]
Список [3, 5, 2, 7] [2, 7] [7] []
X1, X2 X1=3, X2=5 X1=2, X2=7
Xs [2, 7] [] Не соответствуют Не соответствуют
Пусть avg будет маленьким проектом, вычисляющим длину списка действительных чисел. Как и в случае facfun, убедитесь, что avg – консольное приложение. General Project Name: avg UI Strategy: console
36
Откомпилируйте проект, чтобы вставить класс avg в дерево проекта. Затем, отредактируйте avg.pro, как показано ниже. Запустите, используя Run in Window, как и ранее. %File: avg.pro implement avg open core, console domains rList= real*. class predicates len:(rList, real) procedure (i, o). clauses classInfo("avg", "1.0"). len([], 0) :- !. len([_X|Xs], 1.0+S) :- len(Xs, S). run():console::init(), List= read(), len(List, A), write(A), nl. end implement avg goal mainExe::run(avg::run).
Давайте изучим концепции, стоящие за этой маленькой программой. Первое, что вы сделали, это создали домен: domains rList=real*.
Затем, вы определил предикат len/2, который получает список через первый аргумент, и выво‐ дит его длину через второй аргумент. Наконец, вы определили предикат run(), чтобы протестиро‐ вать программу: run() :console::init(), List=read(), len(List, A), write(A), nl.
%Инициализировали консоль %Прочитали List с консоли %Нашли длину списка %Вывели длину на экран
Объявление домена удобно, если вы имеете дело с составными алгебраическими типами дан‐ ных. Тем не менее, это не требуется для таких простых структур, как список действительных чисел. Ниже вы найдёте ту же программу без объявления домена. %File: avg.pro implement avg open core, console class predicates len:(real*, real) procedure (i, o). clauses classInfo("avg", "1.0"). len([], 0) :- !. len([_X|Xs], 1.0+S) :- len(Xs, S). run():- console::init(), L= read(), len(L, A), write(A), nl. end implement avg goal mainExe::run(avg::run).
37
Эта программа имеет недостаток: len/2 можно использовать только для подсчёта длины списка действительных чисел. Было бы неплохо, если бы len/2 мог принимать любой тип списка. Это должно быть возможным, если подставлять переменную типа для real*. Однако, до того, как прове‐ рять, действительно ли эта схема работает, необходимо придумать способ извещать L=read() что будет конечным типом вводимого списка, и передать эту информацию в len/2. К счастью, Visual Prolog имеет предикат, который создаёт переменную любого заданного типа. Ниже вы узнаете, как его использовать. %File: avg.pro implement avg open core, console class predicates len:(Element*, real) procedure (i, o). clauses classInfo("avg", "1.0"). len([], 0) :- !. len([_X|Xs], 1.0+S) :- len(Xs, S). run():- console::init(), hasdomain(real_list, L), L= read(), len(L, A), write(A), nl. end implement avg goal mainExe::run(avg::run).
Следующий шаг – добавить предикаты sum/2 к avg.pro: class predicates sum:(rList, real) procedure (i, o). clauses sum([], 0) :- !. sum([X|Xs], S+X) :- sum(Xs, S).
Давайте посмотрим, что произойдёт, если вызвать sum([3.4, 5.6, 2.3], S). 1. sum([3.4, 5.6, 2.3], S[5.6, 2.3]+X), где S[5.6, 2.3]+X=S[3.4, 5.6, 2.3], соответствует предложению sum([X|Xs], S+X) :- sum(Xs, S), с X=3.4, Xs=[5.6, 2.3], дает sum([3.4, 5.6, 2.3], S[5.6, 2.3]+3.4) :- sum([5.6, 2.3], S[5.6, 2.3]) 2. sum([5.6, 2.3], S[2.3]+X), где S[2.3]+X=S[5.6, 2.3], соответствует предложению sum([X|Xs], S+X) :- sum(Xs, S), с X=5.6, Xs=[2.3], дает sum([5.6, 2.3], S[2.3]+5.6) :- sum([2.3], S[2.3]) 3. sum([2.3], S[]+X), где S[]+X=S[2.3], соответствует предложению sum([X|Xs], S+X) :- sum(Xs, S), с X=2.3, Xs=[], дает sum([2.3], S[]+2.3) :- sum([], S[]) 4. sum([], S[]), соответствует предложению sum([], 0.0) :- !, дает S[]=0. После достижения низа рекурсии, компьютер должен откатиться к началу подсчетов. Что еще хуже, он должен хранить каждый X, который он найдет по пути, чтобы произвести сложение S+X во время отката. Традиционный путь предотвращения отката – использование накопителей. Вот опре‐ деление, которое использует накопитель для суммирования элементов списка: add([], A, A). add([X|Xs], A, S) :- add(Xs, X+A, S).
Давайте посмотрим, как компьютер подсчитывает эту вторую версию суммирования списка. 1. add([3.4, 5.6, 2.3], 0.0, S) соответствует предложению add([X|Xs], A, S) :- add(Xs, X+A, S) дает add([5.6, 2.3], 0+3.4, S)
38
2. add([5.6, 2.3], 0.0+3.4, S) соответствует предложению add([X|Xs], A, S) :- add(Xs, X+A, S) дает add([2.3], 0+3.4+5.6, S) 3. add([2.3], 0.0+3.4+5.6, S) соответствует предложению add([X|Xs], A, S) :- add(Xs, X+A, S) дает add([], 0+3.4+5.6+2.3, S) что соответствует предложению add([], A, A), возвращая S=11.3. Вы можете использовать add/3 для вычисления среднего значения списка чисел. len([], 0) :- !. len([_X|Xs], 1.0+S) :- len(Xs, S). add([], A, A) :- !. add([X|Xs], A, S) :- add(Xs, X+A, S). sum(Xs, S) :- add(Xs, 0.0, S). avg(Xs, A/L) :- sum(Xs, A), len(Xs, L).
Описанная выше программа проходит через список дважды, один раз, чтобы подсчитать сумму, и другой, чтобы подсчитать длину. Используя два накопителя, вы можете вычислять длину и сумму вместе. %File: avg.pro implement avg open core, console class predicates avg:(real*, real, real, real) procedure (i, i, i, o). clauses classInfo("avg", "1.0"). avg([], S, L, S/L). avg([X|Xs], S, L, A) :avg( Xs, X+S, %добавить X к накопителю суммы L+1.0, %увеличить на единицу накопитель длины A). run():- console::init(), hasdomain(rList, List), List= read(), avg(List, 0, 0, A), write(A), nl. end implement avg goal mainExe::run(avg::run).
Вышеприведенный листинг показывает программу для вычисления среднего значения списка действительных чисел. Используется два накопителя, один для суммы, и другой для количества эле‐ ментов.
6.3. •
Схемы обработки списков
Давайте рассмотрим несколько важных схем программирования списков. Редукция. Схема, используемая для вычисления суммы элементов списка, называется редукци‐ ей, потому что она сокращает размерность входных данных1. Фактически, списки можно считать одномерными данными, а сумму – величиной нулевой размерности. Есть два способа выполне‐ ния сокращения: рекурсивный и итеративный. Вот рекурсивная редукция суммы:
1
reduction – англ. сокращение.
39
%Рекурсивная редукция class predicates sum:(real*, real) procedure (i, o). clauses sum([], 0) :- !. sum([X|Xs], S+X) :- sum(Xs, S).
Термин итеративный восходит к латинскому слову iterum, которое означает снова. Вы также можете вообразить, что он восходит к iter/itineris – путь, дорога. Итеративная программа проходит через код снова и снова. %Итеративная редукция add([], A, A) :- !. add([X|Xs], A, S) :- add(Xs, X+A, S). sum(Xs, S) :- add(Xs, 0.0, S).
•
«Молния». Это устройство, используемое в сплошной застежке для одежды, изобретенной ка‐ надским инженером Гидеоном Сандбэком (Gideon Sundback). Это устройство сводит с ума, когда не работает, и является причиной такого количества инцидентов с мальчишками, что врачам и медсёстрам потребовалось специальное обучение, чтобы разбираться с этими устройствами. class predicates dotproduct:(real*, real*, real) procedure (i, i, o). clauses dotproduct([], _, 0) :- !. dotproduct(_, [], 0) :- !. dotproduct([X|Xs], [Y|Ys], X*Y+R) :- dotproduct(Xs, Ys, R).
Пример показывает применение этой схемы для вычисления скалярного произведения двух спи‐ сков. В выражении X*Y+R умножение используется для закрытия одного зажима застежки.
6.4.
Операции со строками.
Ниже вы найдете несколько примеров операций со строками. Этих примеров должно быть дос‐ таточно, чтобы показать вам, как разбираться с подобными структурами данных. Вы можете найти больше операций, используя справочную систему Visual Prolog. implement strings open core, string clauses classInfo("strings", "1.0"). class predicates tokenize:(string, string_list) procedure (i, o). clauses tokenize(S, [T|Ts]) : frontToken(S, T, R), !, tokenize(R, Ts). tokenize(_, []). run():- console::init(), L= ["it ", "was ", "in ", "the ", "bleak ", "December!"], S= concatList(L), UCase= toUpperCase(S), RR=string::concat(“case: “, Ucase), R1=string::concat(“It is “, “upper “, RR), stdio::write(R1), stdio::nl, tokenize(R1, Us), stdio::write(Us), stdio::nl.
40
end implement strings goal mainExe::run(strings::run).
Попытайтесь понять, как работает frontToken, потому что это весьма полезный предикат. Он используется для разбиения строки на token’ы ‐ знаки, в процессе, называемом лексическим анали‐ зом. Например, если вы выполните frontToken(“break December”, T, R)
вы получите T=”break”, и R=” December”. Часто требуется трансформировать строковое представление терма в работоспособное. В этом случае, можно использовать функцию toTerm. В примере ниже Sx=stdio::readLine() читает строку из командной строки в Sx; затем toTerm трансформирует строковое представление в дейст‐ вительное число; вызов hasdomain(real, IX) удостоверит, что toTerm вернёт действительное число. implement typedeterm clauses classinfo("typedeterm", "1.0"). run():- console::init(), Sx= stdio::readLine(), hasdomain(real, IX), IX= toTerm(Sx), stdio::write(IX^2). end implement typedeterm goal mainExe::run(typedeterm::run).
Эта схема работает даже для списков и других составных структур данных, как вы можете видеть ниже. К сожалению, hasdomain не принимает запись типов списков со звёздочкой. Таким образом, вы должны объявить домен списка, или использовать заранее объявлённый домен, такой, как core::integer_list. implement toString clauses classInfo("toString", "1.0"). run():- console::init(), hasdomain(core::integer_list, Xs), Xs= toTerm(stdio::readLine()), stdio::write(Xs), stdio::nl. end implement toString goal mainExe::run(toString::run).
Вы научились преобразовывать строковое представление типов данных в термы Пролога. Те‐ перь, давайте посмотрим, как делать обратную операцию, т. е., преобразовывать терм в его строко‐ вое представление. Если вам нужно стандартное представление терма, то вы можете использовать предикат toString. implement fromTerm clauses classInfo("fromTerm", "1.0"). run():- console::init(), Xs= [3.4, 2, 5, 8], Str= toString(Xs), Msg= string::concat("String representation: ", Str), stdio::write(Msg), stdio::nl. end implement fromTerm goal mainExe::run(fromTerm::run).
41
Если вы хотите покрасивее отформатировать числа и другие простые термы при преобразовании их в строку, вы можете использовать функцию string::format; пример приведён ниже. implement formatting clauses classInfo("formatting", "1.0"). run(): console::init(), Str1= string::format("%8.3f\n %10.1e\n", 3.45678, 35678.0), Str2= string::format("%d\n %10d\n", 456, 18), Str3= string::format("%-10d\n %010d\n", 18, 18), stdio::write(Str1, Str2, Str3). end implement formatting goal mainExe::run(formatting::run).
В примере выше, формат "%8.3f\n означает, что я хочу отобразить действительное число и воз‐ врат каретки; ширина поля 8 и число должно быть отображено с тремя знаками после запятой. Фор‐ мат "%010d\n" требует целого числа, выровненного по правому краю поля шириной 10; пустые мес‐ та поля должны быть заполнены нулями. Формат “%-10d\n” определяет отображение целого числа, выровненного по левому краю поля шириной 10; знак минус удостоверяет левое выравнивание; правое выравнивание является умолчанием. Формат “%10.1e\n” определяет научную запись для действительных чисел. Ниже вы можете увидеть то, что пример выведет на экран.
Ниже вы найдёте список типов данных, принимаемых строкой формата. Отметьте, что когда в текстовое отображение конвертируются действительные числа, они обрезаются и округляются 17 цифрами, если не была указана иная точность. f Форматировать как действительное число с фиксированной запятой (как в 123.4). e Форматировать действительное число в экспоненциальной записи (как в 1.234e+002). g Форматировать в формате f или e, где получается короче. d Форматировать как знаковое целое. u Форматировать как беззнаковое целое. x Форматировать как шестнадцатеричное число. o Форматировать как восьмеричное число. c Форматировать как символ. B Форматировать как двоичный тип Visual Prolog. R Форматировать как номер ссылки базы данных. P Форматировать как параметр процедуры. s Форматировать как строку. Сводка по предикатам. Ниже вы найдёте список других строковых предикатов, которые вы можете найти полезными в разработке ваших приложений.
42
adjust : (string Src, charCount FieldSize, adjustSide Side) -> string AdjustedString. adjust : (string Src, charCount FieldSize, string Padding, adjustSide Side) -> string AdjustedString. adjustLeft : ( string Src, charCount FieldSize) -> string AdjustedString. adjustLeft : ( string Src, charCount FieldSize, string Padding) -> string AdjustedString. adjustRight : ( string Src, charCount FieldSize) -> string AdjustedString. adjustRight : ( string Src, charCount FieldSize, string Padding) -> string AdjustedString. adjustRight : ( string Src, charCount FieldSize, string Padding, adjustBehaviour Behaviour) -> string AdjustedString. adjustBehaviour = expand; cutRear; cutOpposite. adjustSide = left; right. implement stringops clauses classInfo("stringops", "1.0"). run():- console::init(), FstLn= "Rage -- Goddess, sing the rage of Peleus' son Achilles,", Str= string::adjust(FstLn, 60, "*", string::right), stdio::write(Str), stdio::nl. end implement stringops goal mainExe::run(stringops::run).
concat : (string First, string Last) -> string Output procedure (i,i). concatList : ( core::string_list Source) -> string Output procedure (i). concatWithDelimiter : (core::string_list Source, string Delimiter) -> string Output procedure (i,i). create : (charCount Length) -> string Output procedure (i). create : (charCount Length, string Fill) -> string Output procedure (i,i). createFromCharList : (core::char_list CharList) -> string String. implement stringops clauses classInfo("stringops", "1.0"). run(): console::init(), SndLn= [ "murderous", "doomed", "that cost the Achaeans countless losses"], Str= string::concatWithDelimiter(SndLn, ", "), Rule= string::create(20, "-"), stdio::write(Str), stdio::nl, stdio::write(Rule), stdio::nl. end implement stringops goal mainExe::run(stringops::run).
43
equalIgnoreCase : (string First, string Second) determ (i,i). front : (string Source, charCount Position, string First, string Last) procedure (i,i,o,o). frontChar : (string Source, char First, string Last) determ (i,o,o). frontToken : (string Source, string Token, string Remainder) determ (i,o,o). getCharFromValue : (core::unsigned16 Value) -> char Char. getCharValue : (char Char) -> core::unsigned16 Value. hasAlpha : (string Source) determ (i). hasDecimalDigits : (string Source) determ (i). hasPrefix : ( string Source, string Prefix, string Rest) determ (i,i,o). hasSuffix : ( string Source, string Suffix, string Rest) determ (i,i,o). isLowerCase : (string Source) determ (i). isUpperCase : (string Source) determ (i). isWhiteSpace : (string Source) determ. lastChar : (string Source, string First, char Last) determ (i,o,o). length : (string Source) -> charCount Length procedure (i). replace : (string Source, string ReplaceWhat, string ReplaceWith, caseSensitivity Case) -> string Output procedure replaceAll : ( string Source, string ReplaceWhat, string ReplaceWith) -> string Output. replaceAll : ( string Source, string ReplaceWhat, string ReplaceWith, caseSensitivity Case) -> string Output. caseSensitivity = caseSensitive; caseInsensitive; casePreserve. search : (string Source, string LookFor) -> charCount Position determ (i,i). search : (string Source, string LookFor, caseSensitivity Case) -> charCount Position determ (i,i,i). split : (string Input, string Separators) -> string_list. split_delimiter : (string Source, string Delimiter) -> core::string_list List procedure (i,i). subString : (string Source, charCount Position, charCount HowLong) -> string Output procedure (i,i,i). toLowerCase : (string Source) -> string Output procedure (i). toUpperCase : (string Source) -> string Output procedure (i). trim : (string Source) -> string Output procedure (i). Удаляет предшествующие и последующие строке пробелы. trimFront : (string Source) -> string Output procedure (i). Удаляет предшествующие строке пробелы. trimInner : (string Source) -> string Output procedure (i).
44
Удаляет группы пробелов из строки Source. trimRear : (string Source) -> string Output procedure (i). Удаляет следующие за строкой пробелы. implement stringops clauses classInfo("stringops", "1.0"). run(): console::init(), Str= " murderous, doomed ", T= string::trim(Str), stdio::write(T), stdio::nl, T1= string::trimInner(T), stdio::write(T1), stdio::nl. end implement stringops goal mainExe::run(stringops::run).
45
Глава 7: Грамматика Философ Витгенштейн (Wittgenstein), в своем «Логико‐философском трактате» (Tratactus Logicus Philosophicus), выразил мнение, что Мир есть положение вещей. Предметы и объекты имеют свой‐ ства, такие, как месторасположение, целостность1, цвет, и т.д. Набор значений, которые эти свойства принимают, называется состоянием. Например, согласно Евклиду, точка не имеет свойств, кроме местоположения. Значит, состояние точки может быть задано её координатами. Первое предложение Tratactus: The World is all, that is the case – «Мир представляет собой всё, что имеет место». Последнее предложение: Whereof one cannot speak, thereof one must be silent – «о чем нельзя говорить, о том нужно молчать». Практически невозможно создать структуру данных для того, чтобы отобразить идеи Витгенштейна на компьютере. Хотя, даже задуманная лингвистическим гением, эта структура данных определённо будет иметь три конструктора: узла world, узла case и узла silent. Вот семантический класс, чьё единственное применение – в экспорте семантического домена Tratactus’а: class semantic open core domains category=art; nom; cop; rel. tree=case(category, string); world(tree, tree); silence. predicates classInfo : core::classInfo. end class semantic
В этом листинге есть новшество: использование конструкторов структур данных. Конструктор имеет вид функции, то есть, у него есть функтор и аргументы. Например, конструктор case/2 имеет два аргумента, грамматическую категорию и строку, обозначающую знак.
7.1.
Грамматический анализ
Действовать значит изменять свойства объектов. Таким образом, результат действия ‐ измене‐ ние состояния. Например, если предикат разбирает список слов, мир переносит изменение состоя‐ ния. В последнем состоянии, инструмент разбора поглотил часть списка. Чтобы понять эти вещи, да‐ вайте создадим класс german, чтобы разобрать первое предложение Tratactus. Используя его, как модель, вы можете написать более мощный класс, чтобы разобрать целую книгу. Верное дело, по‐ тому что Tratactus очень короткий. %File: german.cl class german open core, semantic predicates classInfo : core::classInfo. article:(tree, string*, string*) nondeterm (o, i, o). noun:(tree, string*, string*) nondeterm (o, i, o). nounphr:(tree, string*, string*) nondeterm (o, i, o). copula:(tree, string*, string*) nondeterm (o, i, o). phr:(tree, string*, string*) nondeterm (o, i, o). end class german %File: german.pro implement german open core, semantic clauses
1
Оно целое или разбитое? – прим. авт.
46
end
classInfo("german", "1.0"). article(case(art, ""), ["die"|Rest], Rest). article(case(art, ""), ["der"|Rest], Rest). noun(case(nom, "Welt"), ["Welt"|R], R). noun(case(nom, "Fall"), ["Fall"|R], R). nounphr( world(T1, T2), Start, End) : article(T1, Start, S1), noun(T2, S1, End). nounphr( case(nom, "alles"), ["alles"|R], R). copula(world(case(cop, "ist"), T), ["ist"|R1], R) : nounphr(T, R1, R). phr(world(T1, T2), Start, End) : nounphr(T1, Start, S1), copula(T2, S1, End). implement german
Если вы обратите внимание на объявление article, вы заметите, что его шаблон потока (o, i, o), то есть, он получает список слов и выводит дерево разбора (parse tree1), и результат поглощения части входного списка. Определение noun работает так же, как article. Пример стоит тысячи слов; давайте подставим ["die", "Welt", "ist", "alles"] в article/3. article(T1, ["die", "Welt", "ist", "alles"], S1).
Этот вызов будет соответствовать первому предложению article/3, с Rest=["Welt", "ist", "alles"].
Выводимым будет T1=case(art, ""), и S1= ["Welt", "ist", "alles"]. Дальше, давайте скормим S1= ["Welt", "ist", alles"], в noun/3. noun(T2, ["Welt", "ist", "alles"], End)
Вышеприведённый вызов будет соответствовать первому предложению noun/3, с R=["ist", "alles"]. Выводимым будет T2=case(nom, "Welt"), и End=["ist", "alles"]. Определение nounphr делает эти два вызова по очереди, производя группу существительного ["die", "Welt"]. copula/3 и phr/3 ведут себя как nounphr.
7.2.
Грамматический синтез
В то время, как немецкий разбор принимает предложение и выводит узел, английский разбор, приведённый ниже, принимает узел, чтобы создать предложение. Можно сказать, что узел пред‐ ставляет собой значение предложения. Немецкий разбор поглощает предложение на немецком, и выводит его значение. Английский разбор вводит значение и производит английский перевод. class english open core, semantic predicates classInfo : core::classInfo. article:(tree, string*, string*) nondeterm (i,o,i)(i,o,o)(o,i,o)(i,i,o). noun:(tree, string*, string*) nondeterm (i,o,i)(i,o,o)(i,i,i)(i,i,o)(o,i,o). nounphr:(tree, string*, string*) nondeterm (i,o,i)(i,o,o)(o,i,o)(i,i,i)(i,i,o).
1
Если вы не знаете эти лингвистические термины, просмотрите книги об обработке естественных языков, или построению компиляторов, что вам больше нравится. – прим. авт.
47
copula:(tree, string*, string*) nondeterm (o,i,o)(i,i,i)(i,i,o). phr:(tree, string*, string*) nondeterm (o,i,o)(i,o,i)(i,o,o). end class english implement english open core, semantic clauses classInfo("english", "1.0"). article(case(art, ""), ["the"|Rest], Rest). noun(case(nom, "Welt"), ["world"|R], R). noun(case(nom, "Fall"), ["case"|R], R). noun(case(nom, "alles"), ["all"|R], R). nounphr( world(T1, T2), Start, End) : article(T1, Start, S1), noun(T2, S1, End). nounphr( case(nom, "alles"), ["all"|R], R). copula(world(case(cop, "ist"), T), ["is"|R1], R) : nounphr(T, R1, R). phr(world(T1, T2), Start, End) : nounphr(T1, Start, S1), copula(T2, S1, End). end implement english
Хорновы предложения, которые выдают фразу, соответствующую грамматике заданного языка, называются production rules ‐ правила вывода. В классе english article/2, noun/2 и nounphr явля‐ ются примерами правил вывода. Каждое английское правило вывода выводит список слов. Имея это в виду, давайте взглянем на правило вывода для phr/2. phr(world(T1, T2), list::append(Start, S1)) :nounphr(T1, Start), copula(T2, S1).
Второй аргумент головы использует list::append(Start, S1) для конкатенации именной группы и связки (copula), выдавая фразу. Чтобы реализовать и протестировать программу, создайте консольный проект tratactus. Вставьте классы german, english, и semantic в дерево проекта. Не забудьте снять галочку Create Objects. Добавьте данный код в соответствующие файлы классов. Тестирующая программа дана на рис. 7.1. После её компиляции, если вы хотите проверить её изнутри Visual Prolog IDE, используйте пункт Build/Run in Window.
7.3.
Почему Пролог?
Одними из целей компьютерных языков высокого уровня являются увеличение способности про‐ граммистов рассуждать, для достижения более высокой производительности, и искусственный ин‐ теллект. Хотя есть единодушное мнение, что Пролог соответствует этим целям более чем любой дру‐ гой язык, есть некоторые споры, окружающие эти термины. Таким образом, давайте посмотрим, что мы понимаем под большей производительностью и искусственным интеллектом. Большая производительность. Продуктивный программист поставляет больше функциональ‐ ности в меньшее время. В программном проекте, такие структуры данных, как списки и деревья не‐ обходимы везде, но являются сложными для кодирования и обслуживания в языках вроде C или Java. В Прологе нет ничего легче, чем работать со списками или деревьями. Грамматический разбор это ещё одна сложная тема, без которой жить не могут; между прочим, разбор это то, что вы делали для того, чтобы реализовать маленький грамматический справочник, который вы изучили в этой гла‐ ве. Дизайн логики приложения также очень громоздок, но мне не нужно развивать эту тему, потому что в Прологе Логика есть даже в названии.
48
Искусственный интеллект делает ваши программы высоко приспосабливаемыми к изменени‐ ям и более дружественными к пользователю. Пролог, бок о бок с Lisp и Scheme, очень популярен среди людей, которые работают с искусственным интеллектом. Это означает, что вы найдёте библио‐ теки Пролога для почти любого алгоритма AI.
7.4.
Примеры
Пример для этой главы (tratactus) показывает, как можно написать программу, которая перево‐ дит философский немецкий на английский.
implement tratactus open core, console, semantic class predicates test:(). clauses classInfo("tratactus", "1.0").
test() :german::phr(T, [ "die", "Welt", "ist", "alles", "was", "der", "Fall", "ist"], _X), write(T), nl, english::phr(T, Translation, []), !, write(Translation), nl, test().
run():- console::init(), test(). end implement tratactus goal mainExe::run(tratactus::run).
Рисунок 7.1. Die Welt ist alles, was der Fall ist.
49
Глава 8: Рисование В этой главе вы научитесь рисовать, используя обработчик события onPaint. Начните с создания проекта с формой, на которой вы будете рисовать. • Создайте проект: Project Name: painting Object-oriented GUI (pfc/gui)
• • •
Создайте пакет paintPack, присоединенный к корню дерева проекта. Вложите canvas.frm внутрь paintPack. Build/Build приложение. Включите пункт File/New панели задач приложения, и добавьте
clauses onFileNew(S, _MenuTag) : X= canvas::new(S), X:show().
к TaskWindow/Code Expert/Menu/TaskMenu/id_file/id_file_new. После этого шага, если вы откомпилируете и запустите программу, когда вы выберете пункт File/New, будет выполнен вышеприведенный предикат. Команда X=canvas::new(S) создаёт новый объект X класса window. Это окно будет дочерним окну задач S. Команда X:show() посылает сооб‐ щение объекту X показать окно. Что вы будете делать дальше? Если бы я был вами, я бы нарисовал что‐нибудь на canvas, чтобы создать интересный задний фон.
8.1.
onPainting
Когда окну требуется прорисовка, оно вызывает обработчик события onPainting. Поэтому, если вы добавите инструкции к onPainting, они будут выполнены. Добавьте onPaint(_Source, _Rectangle, GDIObj) :dopaint::bkg(GDIObj).
к ProjectWindow/canvas.frm/Code Expert/Window/Paint→onPaint. Как я сказал ранее, когда окну требуется прорисовка, оно вызывает onPaint, который вызывает dopaint::bkg(GDIObj). Класс dopaint не существует. Я намереваюсь снабдить его методом bkg(GDIObj), который рисует на ок‐ не, указанном GDIObj. • Создайте класс dopaint внутри paintPack. Отключите Creates Objects. • Добавьте приведённый ниже код к dopaint.cl и dopaint.pro. % File dopaint.cl class dopaint open core predicates bkg:(windowGDI). end class dopaint %File: dopaint.pro implement dopaint open core, vpiDomains clauses bkg(W) : P= [pnt(0,0), pnt(10,80), pnt(150, 100)], W:drawPolygon(P). end implement dopaint
50
Давайте поймём, что делает метод bkg(W). Как вы можете увидеть, P хранит список точек. Каж‐ дая точка определена своими координатами. Например, pnt(0, 0) это точка в (0, 0). Когда bkg(W) вызывает W:drawPolygon(P), он посылает сообщение объекту W, запрашивая у него рисование мно‐ гоугольника, чьи вершины заданы списком P. Откомпилируйте программу, и проверьте, будет ли она работать в точности, как сказано. Давайте улучшим метод bkg(W). Он рисует белый треугольник на поверхности canvas. Чтобы сделать треугольник красным, нужно сменить кисть рисования. Кисть представляет собой двухаргу‐ ментный объект. brush=brush(patStyle PatStyle, color Color).
Цвета представлены числами, определяющими количество красного, зеленого и синего. Как вы, возможно, знаете, вы можете получить много цветов, комбинируя красную, зеленую и синюю палит‐ ры. Давайте представим числа в шестнадцатеричном базисе. Шестнадцатеричные числа имеют ше‐ стнадцать цифр: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F. Красный цвет задан числом 0x000000FF, где 0x показывает, что мы имеем дело с шестнадцатеричным базисом. Отображать цвета числами не‐ плохо, но вы также имеете возможность использовать названия. Например, color_Red представляет красный цвет, как вы можете предположить. Вот также названия для шаблонов: • pat_Solid: сплошная кисть. • pat_Horz: горизонтальная штриховка. • pat_Vert: вертикальная штриховка. • pat_FDiag: наклонная под 45° штриховка. Вот модификация bkg(W), выдающая красный треугольник: bkg(W) :Brush= brush(pat_Solid, color_Red), W:setBrush(Brush), P= [pnt(0,0), pnt(10,80), pnt(150, 100)], W:drawPolygon(P).
Наконец, вот версия bkg(W), которая чертит эллипс и очищает в нём зеленый прямоугольник. bkg(W) :R= rct(40, 60, 150, 200), W:drawEllipse(R), R1= rct( 60, 90, 140, 130), W:clear(R1, color_Green).
Последнее, чему вы научитесь в этой главе, это как добавлять bmp изображение к вашему окну. Вот как надо модифицировать метод bkg(W): bkg(W) :P= vpi::pictLoad("frogs.bmp"), W:pictDraw(P, pnt(10, 10), rop_SrcCopy).
Первый шаг – загрузить изображение из файла. Это сделано использованием предиката pictLoad/1. Следующий шаг – нарисовать изображение. В примере, изображение P размещено в точке pnt(10, 10).
51
Рисунок 8.1 frogs.bmp
Рисунок 8.2 A3mask.bmp
Рисунок 8.3 A3.bmp
Часто необходимо нарисовать маленькое изображение поверх другого, убрав задний план ма‐ ленького изображения. Например, вы можете захотеть вставить орла среди лягушек из предыдущего примера. Первый шаг – нарисовать маску орла используя rop_SrcAnd. Маска показывает черную тень ор‐ ла на белом фоне. Следующий шаг – нарисовать самого орла, используя rop_SrcInvert. Орёл дол‐ жен совершенно точно соответствовать своей маске. Программа, вставляющая орла среди лягушек, приведена ниже, и реализована в папке pain tEagle, в примерах. bkg(W) :P= vpi::pictLoad("frogs.bmp"), W:pictDraw(P, pnt(10, 10), rop_SrcCopy), Mask= vpi::pictLoad("a3Mask.bmp"), Eagle= vpi::pictLoad("a3.bmp"), W:pictDraw(Mask,pnt(10, 10),rop_SrcAnd), W:pictDraw(Eagle,pnt(10, 10),rop_SrcInvert).
8.2.
Пользовательские элементы управления
Мы рисовали напрямую на полотне формы. Тем не менее, хорошей идеей будет создать пользо‐ вательский элемент управления, и использовать его для рисования. • Создайте новый проект Project Name: customcontrol Object-oriented GUI (pfc/gui)
•
Выберите пункт File/New из панели задач VDE. В диалоговом окне Create Project Item, выберите элемент Control на панели по левую руку. Названием нового элемента управ‐ ления будет canvas.
Name: canvas New Package Parent Directory [
•
•
]
Оставьте поле Parent Directory пустым. Нажмите кнопку Create. Примите настройки по умолчанию диалогового окна IDE Control Properties. На экране появится прототип полотна. Вы можете закрыть его, чтобы избежать загромождения VDE. Откомпилируйте приложение, чтобы включить в него новый элемент управления. Добавьте новую форму к дереву проекта. Выберите File/New из панели задач и выбе‐ рите пометку Form из панели по левую руку диалогового окна Create Project Items. Пусть название новой формы будет greetings. После принятия установок по умолчанию вы получите прототип формы greetings.
52
•
Выберите символ ключа, который появится на панели Controls. Щёлкните на поверхно‐ сти формы greetings. Система покажет вам меню, содержащее список нестандартных элементов управления; элемент canvas – первый.
•
Откомпилируйте приложение. Добавьте
clauses onMouseDown(S, Point, _ShiftControlAlt, _Button) : W= S:getVPIWindow(), Point= pnt(X, Y), vpi::drawText(W, X, Y, "Hello!").
•
к ProjectTree/canvas.ctl/Code Expert/Mouse/MouseDown. Снова откомпилируйте приложение. Включите пункт TaskMenu.mnu→File/New. Добавьте
clauses onFileNew(S, _MenuTag) : F= greetings::new(S), F:show().
к TaskWindow.win/Code Expert/Menu/TaskMenu/id_file/id_file_new. Откомпилируйте и запустите приложение. Выберите пункт File/New и появится новая форма. Ко‐ гда вы щёлкнете в поле canvas на новой форме, оно напечатает тёплые приветствия.
53
Глава 9: Типы данных Большинство современных языков работают со строго типизированными данными. Это означает, что компилятор проверяет, принадлежат ли данные, по‐ даваемые функции, предикату или команде, верному типу. Например, арифме‐ тические операции , , , работают с целыми, действительными, или комплекс‐ ными числами. Поэтому, компилятор удостоверяется, что ваша программа передаёт числа и ничего другое этим операциям. Если в вашем коде есть логическая ошибка, и вы попытаетесь разделить строку на число, компилятор заартачится. Я уверен, что вы и сами скажете, что это единственное ра‐ зумное, что можно сделать. Я согласен. Тем не менее, не все разработчики языков сходятся в жела‐ нии включать проверку типов в их компиляторы. К примеру, Standard Prolog не проверяет тип до тех пор, пока ошибочный код не будет запущен. Программа падает, когда она уже на компьютере ко‐ нечного пользователя.
9.1.
Примитивные типы данных
Visual Prolog имеет множество примитивных типов данных: integer: 3, 45, 65, 34, 0x0000FA1B, 845. Между прочим, 0x0000FA1B – шестнадцатеричное число, то есть, число выраженное в базисе 16, которое имеет следующие цифры: 0, 1, 2,3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F. real: 3.45, 3.1416, 2.18E‐18, 1.6E‐19, и т. д. string: "pencil", "John McKnight", "Cornell University", и т.д. symbol : "Na", "Natrium", "K", "Kalium", и т.д. Вы заметили, что символы выглядят очень похоже на строки: оба представляют собой последо‐ вательности Unicode знаков. Однако хранятся они по‐разному. Символы хранятся в таблице, и Пролог использует их адреса в таблице для их внутреннего представления. Таким образом, если символ встречается в программе много раз, он будет занимать меньше места, чем строка. Ниже, приведен очень простой пример использования действительных чисел, символов и строк. Этот пример взят из химии. % File primtype1.pro implement primtype1 open core constants className = "primtype1". classVersion = "". clauses classInfo(className, classVersion). class predicates energy:(real, real, real) procedure (i, i, o). family:(symbol, string) procedure (i, o). clauses energy(N, Z, E) :- E= -2.18E-19*Z*Z/(N*N). family("Na", "alkaline metal") :- !. family(_, "unknown"). run():- console::init(), energy(4, 1, E1), energy(2, 1, E2), DeltaE= E1-E2, stdio::write(DeltaE), stdio::nl,
54
family("Na", F), stdio::write("Na", " is an ", F), stdio::nl, succeed(). end implement primtype1 goal mainExe::run(primtype1::run).
9.2.
Множества
Множества – это коллекции/собрания вещей. Вы определённо знаете, что коллекции не имеют повторяющихся предметов. Я имею в виду, если у кого‐то есть коллекция наклеек, он(а) не захочет иметь две копии одной и той же наклейки в его/её коллекции. Если у него/неё есть повторяющийся предмет, он(а) поменяет его на другой предмет, которого недостаёт в его/её коллекции. Возможно, что если у вашего отца есть коллекция греческих монет, он с удовольствием примет ещё одну драхму в свой набор. Однако, две драхмы не в точности одинаковы; одна из них может быть старше другой.
9.3.
Множества чисел
Математики собирают другие вещи, отличающиеся от монет, марок и логарифмических линеек. Например, они собирают числа; следовательно, от вас ожидается изучить многое о множествах чи‐ сел. является множеством натуральных чисел. Вот как математики записывают элементы : 0, 1, 2, 3, 4 … 1. является множеством целых чисел, т. е., … 3, 2, 1,0,1,2,3,4 … . Почему множество целых чисел изображается буквой ? Я не знаю, но я могу дать образованное предположение. Теория множеств была открыта Георгом Фердинанд Людвиг Филипп Кантором (Georg Ferdinand Ludwig Philipp Cantor), русским, чьими родителями были датчане, но кто написал свою Mengenlehre на немецком! На этом языке целые числа могут иметь такое странное имя как Zah len. Вы можете подумать, что теория множеств скучна; однако, много людей думают, что она до‐ вольно интересна. Например, есть аргентинец, которого школьники считают величайшм писателем, жившим после падения греческой цивилизации. Короче, только греки могут выдвинуть лучшего ав‐ тора. Вы, возможно, слышали чилийцев, говоривших, что аргентинцы несколько самодовольные. Знаете, какая сделка самая лучшая? Это заплатить честную цену за аргентинцев, а потом пере‐ продать их за то, что они посчитают своей ценой. Однако, вопреки мнению чилийцев, Хорхе Луис Борхес (Jorge Luiz Borges) был величайшим писателем, кто писал на языке, отличном от греческого. Вы знаете, какая у него была любимая тема? Это была Теория Множеств, или Der Mengenlehre, как он любил её называть. Пожалуйста, мои аргентинские друзья, не злитесь. В конце концов, ваша страна четвёртая в спи‐ ске моих любимых стран, после Греции, Франции и Парагвая. Положение Парагвая в моём списке высоко, единственно потому, что Хосе Асунсьон Флорес(José Asunción Flores) был парагвайцем; для тех, кто не знает Хосе Асунсьон Флореса, послушайте его песню India, и, я уверен, что вы отметите Парагвай на своей карте. Что до Аргентины, то это родина Бернардо Усая2 (Bernardo Houssay), Луиса Федерико Лелуара3 (Luis Federico Leloir), Сезара Мильштейна1 (César Milstein), Адольфо Перез Эскиве‐ 1
Обычно считается, что ноль не принадлежит множеству натуральных чисел, то есть : 1, 2, 3, 4 … но : 0, 1, 2, 3, 4 … . Тем не менее, существует и такая запись: : 0,1, 2, 3, 4 … но : 1, 2, 3, 4 … В программировании действительно удобнее нумеровать с 0, поэтому автор использует множество натуральных чисел, содержащих ноль. 2 Бернардо Альберто Усай – аргентинский физиолог, лауреат Нобелевской премии по физиологии и медицине 1947 года. 3 Луис Федерико Лелуар – аргентинский биохимик французского происхождения, лауреат Нобелевской премии 1970 года.
55
ла (Adolfo Pérez Esquivel), Карлоса Сааведра Ламаса (Carlos Saavedra Lamas), Хорхе Луис Борхеса (Jorge Luiz Borges), Альберто Кальдерона (Alberto Calderon), Альберто Гинастеры (Alberto Ginastera), Хосе Ку‐ ра (José Cura), Дарио Волонте (Dario Volonté), Вероники Дахл (Verónica Dahl), Каролины Монард (Caro lina Monard), Че Гевары (Che Guevara), Диего Марадонны (Diego Maradona), Хуана Мануэль Фангио (Juan Manuel Fangio), и т. д.. Пока я не забыл, Хосе Асунсьон Флорес жил в Буэнос‐Айресе. Но давайте вернёмся к множествам. Когда математик хочет сказать, что элемент является членом множества, он пишет что‐то напо‐ добие 3 Если он хочет сказать, что что‐то не является элементом множества, например, он хочет сказать, что ‐3 не элемент , он напишет: 3 Давайте резюмируем обозначения, которые используют учителя алгебры, когда читают своим студентам теорию множеств. Двойная вертикальная черта. Странная запись отображает множество таких, что x является элементом , или, иначе, множество 0,1,4,9,16,25 … Guard. Если вы хотите сказать, что x является элементом при условии, что 10, вы можете написать 10 . Коньюнкция. В математике вы можете использовать символ чтобы сказать И; таким образом, 2 5 означает, что 2 И 5. Дизъюнкция. Выражение 2 5 означает 2 ИЛИ 5. Используя вышеприведённые обозначения, вы можете определить множество рациональных чисел: 0 На неформальном языке это выражение означает, что рациональное число – это дробь вида такая, что p является членом и q также является элементом множества 2, при условии, что q не равно 0. Visual Prolog не имеет специального обозначения для множеств, но вы можете использовать спи‐ ски для их представления. Например, выберите пункт Project/New из панели задач и заполните диа‐ логовое окно Project Settings так: General Project Name: zermelo UI Strategy: console
Обратите внимание, что мы собираемся использовать консольную стратегию, не GUI. Выберите пункт Build/Build из панели задач чтобы вложить прототип класса zermelo в дерево проекта. Отре‐ дактируйте zermelo.pro как показано ниже. Снова откомпилируйте проект и запустите его, исполь‐ зуя Build/Run In Window. implement zermelo open core clauses classInfo("zermelo", "1.0"). run():-
1
Русской транскрипции не нашёл, биохимик, поделил Нобелевскую премию по физиологии или медицине (пишется вместе) в 1984 году с Niels K. Jerne и Georges Köhler. 2 В оригинале автор указывает как числитель, так и знаменатель рациональной дроби как элементы множества (оба). Но это несущественно, так как если числитель , то эта дробь уже может изображать как положительные, так и отрицательные рациональные числа, а из множества уже исключён ноль.
56
console::init(), Q= [tuple(X, Y) || X= std::fromTo(1, 4), Y= std::fromTo(1, 5)], stdio::nl, stdio::write(Q), stdio::nl, stdio::nl, foreach tuple(Num, Den)= list::getMember_nd(Q) do stdio::write(Num, "/", Den,", ") end foreach, stdio::nl. end implement zermelo goal mainExe::run(zermelo::run).
Вы можете спросить, почему я назвал программу именем немецкого математика Эрнста Фрид‐ рих Фердинанд Цермело (Ernst Friedrich Ferdinand Zermelo). Одной из причин может быть то, что он был назначен на почётную должность во Фра́йбурге в Бр́айсгау (Freiburg im Breisgau) в 1926, который он покинул в 1935 за то, что осуждал режим Гитлера. Другая причина в том, что он изобрёл запись множеств, которую мы использовали. Q= [tuple(X, Y) || X= std::fromTo(1, 4), Y= std::fromTo(1, 5)],
В записи Цермело это выражение записывается так: , 1…4 1…5 На обычном русском, Q это список пар (X, Y), таких, что X принадлежит 1,2,3,4 , и Y принадлежит 1,2,3,4,5 . Фрагмент foreach tuple(Num, Den)= list::getMember_nd(Q) do stdio::write(Num, "/", Den,", ") end foreach,
говорит компьютеру write(Num, "/", Den,", ") для каждого кортежа (Num, Den), которые являются членами списка Q. Множество рациональных чисел так называется, потому что его элементы могут быть представ‐ лены в виде дроби вроде где p и . Следующий раздел несколько сложен для усвоения. Поэтому, если вы хотите его пропустить, вы вольны это сделать. В разделе о Действительных Числах я подведу важные итоги, поэтому вы не пропустите чего‐либо стоящего понимания целой страницы противной математики.
9.4.
Иррациональные числа Во времена Пифагора, древние греки заявляли, что любая пара ли‐ нейных отрезков соизмерима, то есть, вы всегда можете найти меру та‐ кую, что длины любых двух отрезков будут даны целыми числами. Сле‐ дующий пример поможет вам понять греческую теорию соизмеримых длин в работе. Рассмотрим рис. 9.1. Если бы греки (жившие во времена Пифагора – прим. пер.) были пра‐ вы, я смог бы найти меру, возможно очень малую, которая производит целое измерение диагонали квадрата, и другое целое измерение для роны. Предположим, что p – результат измерения стороны квадрата, а q – результат измерения диагонали. Теорема Пифагора утверждает, что , т. е., 9.1
57
Вы также можете выбрать меру так, что p и q не будут иметь общих множителей. Например, если и p и q делимы на 2, вы можете удвоить длину меры, получая значения более не делимые на 2. Например, если p=20 и q=18, и вы удваиваете длину меры, вы получаете p=10 и q=9. Таким образом, допустим, что вы‐ брана мера такая, что p и q одновременно не чётные. Но из уравнения 9.1 q2 чётное. Но если q2 чётное, q тоже чётное. Вы можете проверить, что квадрат нечётного числа всегда нечётное число. Так как q чётное, вы можете подставить 2×n вместо него в уравнение 9.1. 2 2 4 2 4 2 9.2 Уравнение 9.1 показывает, что q чётное; уравнение 9.2 доказывает, что p тоже чётное. Но это противоречит нашему допущению, что p и q не одновременно чётные. Следовательно, p и q не могут быть целыми чис‐ лами в уравнении 9.1, которое вы можете переписать как Рисунок 9.1 Думающий грек √2 Число √2, которое даёт отношение между стороной и диагональю любого квадрата, не элемент , или иначе, √2 . Тем, кто доказал это, был Гиппас из Метапонтума (Hippasus of Metapontum), греческий философ. Греки были народом мудрых мужчин и женщин. Несмотря на это, они имели странную привычку советоваться с необразованной крестьянкой в Дельфах, перед тем, как сделать что‐то полезное. Хра‐ ня эту традицию, Гипас спросил Дельфийскую жрицу – необразованную девчонку – что ему нужно сделать, чтобы удовлетворить Аполлона. Она сказала ему измерить сторону и диагональ квадратного алтаря бога, используя одну и ту же меру. Доказав то, что проблема неразрешима, Гипас открыл тип чисел, которые не могли быть записаны как дроби. Числа такого рода называются иррациональны‐ ми.
9.5.
Действительные числа
Множество всех чисел, целых, иррациональных и рациональных называется , или множеством действительных чисел. В Visual Prolog, называется integer – целым, хотя множество integer не покрывает все целые числа, но достаточно из них, чтобы удовлетворить вашим нуждам. Действи‐ тельные числа Visual Prolog принадлежат множеству real, подмножеству . Если , программисты на Visual Prolog говорят, что x имеет тип integer. Они также говорят, что r имеет тип real если . Есть также другие типы, кроме integer и real. Вот список примитивных типов. integer – Целые числа между -2147483648 and 2147483647. real – Действительные числа должны быть записаны с десятичной точкой: 3.4, 3.1416, etc. string – Заключённая в кавычки строка символов: "3.12", "Hippasus", "pen", etc. char – Символы в одинарных кавычках: 'A', 'b', '3', ' ', '.', etc.
9.6.
Форматирование
Метод format создаёт неплохие представления примитивных типов данных для вывода. К при‐ меру, хорошо бы отображать цвета, используя шестнадцатеричные числа. В этом представлении, ос‐ новные цвета становятся 0x00FF0000 (красный), 0x0000FF00 (зелёный), и 0x000000FF (синий). Тем не менее, если вы попытаетесь вывести эти значения, используя stdio::write/n, вы получите их в десятеричном базисе. Используя string::format/n, как показано ниже, вы получите распечат‐ ку примитивных цветов в шестнадцатеричном виде. % File primtype2.pro implement primtype2 open core constants
58
className = "primtype2". classVersion = "". clauses classInfo(className, classVersion). class predicates color:(symbol, integer) procedure (i, o). clauses color("red", 0x00FF0000) :- !. color("blue", 0x000000FF) :- !. color("green", 0x0000FF00) :- !. color(_, 0x0). run():- console::init(), color("red", C), S= string::format("%x\n", C), stdio::write(S), stdio::nl, succeed(). end implement primtype2 goal mainExe::run(primtype2::run).
Первый аргумент функции format определяет, какой вы ходите видеть распечатку. В примере аргумент “%x\n”: вы хотите шестнадцатеричное отображение числа, завершающееся возвратом ка‐ ретки (\n). Аргументы, следующие за первым, показывают данные, которые вы хотите напечатать. Вот несколько примеров форматирования: S= string::format("Pi=%4.3f\n", 3.14159) S= “Pi=3.142\n” S= string::format("%4.3e\n", 33578.3) S= ”3.358e+004\n” S= string::format("Fac(%d)=%d", 5, 120) S= “Fac(5)=120” S= string::format("%s(%d)=%d", "f", 5, 120) S= “f(5)=120” Спецификацией поля форматирования является %[-][0][width][.precision][type], где дефис (‐) означает, что поле будет выровнено по левому краю, выравнивание по правому краю при‐ нято по умолчанию; ноль перед width заполняет форматированную строку нулями до тех пор, пока не будет достигнута минимальная ширина; width определяет минимальный размер поля; precision определяет точность числа с плавающей точкой; наконец, type может быть f (число с фиксированной точкой), e (научная запись действительных чисел), d (десятичное целое), x (шестнадцатеричное це‐ лое), o (восьмеричное). Строки отображаются как “%s”. Вы можете добавить любое сообщение в спецификацию формата; вы также можете добавить возврат каретки (“\n”).
9.7.
Домены
Вам могут потребоваться иные типы данных кроме примитивов, предложенных Visual Prolog. В этом случае, вам нужно определить новые типы данных, которые вы будете помещать в ваш код.
9.7.1. Списки Вы узнали, что списки это упорядоченные последовательности элементов. Вы также знаете, как создавать списочные домены и объявлять предикаты на списках. Например, вот несколько списоч‐ ных доменов: domains reals= real*. % Список действительных чисел integers= integer*. % Список целых чисел strings= string*. % Список строк rr= reals*. % Список списков действительных чисел
59
Когда у вас есть объявление домена, вы можете использовать его как любой другой примитив‐ ный тип. В случае списков, объявление домена не обязательно, так как можно использовать типы вроде real*, string* или integer* прямо в объявлении предиката, как в программе на рис. 9.2.
9.7.2. Функторы Исчисление предикатов, ветвь логики, имеет тип данных, называемый функциональный символ (functional symbol). Функциональный символ имеет название, или функтор, за которым следуют аргу‐ менты. Аргументы записываются между круглыми скобками, и отделяются друг от друга запятыми. Говоря проще, они имеют в точности ту же форму записи, что и функции. Вот несколько примеров функциональных символов: • author("Wellesley", "Barros", 1970) • book( author("Peter", "Novig", 1960),"Artificial Intelligence") • date("July", 15, 1875) • entry("Prolog", "A functional language") • index("Functors", 165) • sunday
Последний пример показывает, что функциональный символ может вообще не иметь аргумен‐ тов. Об этом сказали, давайте теперь посмотрим, как объявлять типы данных для новых функторов. Пример вычисляет день недели для любой даты между годами 2000 и 2099. Он имеет следующие объявления доменов: ДОМЕН Комментарии year= integer. year является синонимом integer. Использование синонимов делает вещи яснее. day= sun; mon;tue; wed; thu; fri; sat; err. Этот аргумент имеет множество функциональных символов без аргументов. Каждый функциональ‐ ный символ отделен от следующего точкой с за‐ пятой и отображает день недели. month= jan; feb; mar; apr; may; jun; jul; aug; sep; Ещё один домен (тип) с более чем одним функ‐ oct; nov; dec. циональным символом. month code= m(month, integer). Здесь, имеем функциональный символ с двумя аргументами. numberOfDays= nd(integer);february. Другой тип с двумя функциональными символа‐ ми: первый функциональный символ имеет один аргумент; второй не имеет ни одного.
60
% File newdomains.pro implement newdomains open core constants className = "newdomains". classVersion = "". class predicates sum:(real*, real) procedure (i, o). clauses classInfo(className, classVersion). sum([], 0) :- !. sum([X|Xs], X+S) :- sum(Xs, S). clauses run():- console::init(), sum([1, 2, 3, 4, 5, 6], A), stdio::writef("The sum is %-4.0f", A). end implement newdomains goal mainExe::run(newdomains::run).
Рисунок 9.2 Объявление списка
% File: func.pro % This program calculates the day of the week % for the years between 2000 and 2099. implement func open core constants className = "func". classVersion = "". domains year= integer. day= sun; mon;tue; wed; thu; fri; sat; err. month= jan;feb;mar;apr;may;jun;jul;aug;sep;oct;nov;dec. month_code= m(month, integer). numberOfDays= nd(integer); february. clauses classInfo(className, classVersion). class predicates monthCode:(string, month_code, numberOfDays) determ (i, m(o,o), nd(o)) (i, m(o, o), o). dayOfWeek:(integer, day) procedure (i, o). calculateDay:( string Month, integer Day_of_the_month, year, day) procedure (i, i, i, o).
61
clauses monthCode("January", m(jan, 6), nd(31)) :- !. monthCode("February", m(feb, 2), february ) :- !. monthCode("March", m(mar, 2), nd(31)) :- !. monthCode("April", m(apr, 5), nd(30)) :- !. monthCode("May", m(may, 0), nd(31)) :- !. monthCode("June", m(jun, 3), nd(30)) :- !. monthCode("July", m(jul, 5), nd(31)) :- !. monthCode("August", m(aug, 1), nd(31)) :- !. monthCode("September", m(sep, 4), nd(30)) :- !. monthCode("October", m(oct, 6), nd(31)) :- !. monthCode("November", m(nov, 2), nd(30)) :- !. monthCode("December", m(dec, 4), nd(31)) :- !. dayOfWeek(0, sun) :- !. dayOfWeek(1, mon) :- !. dayOfWeek(2, tue) :- !. dayOfWeek(3, wed) :- !. dayOfWeek(4, thu) :- !. dayOfWeek(5, fri) :- !. dayOfWeek(6, sat) :- !. dayOfWeek(_, err). calculateDay(Month, MD, Year, D) : Y= Year-2000, % Берём две последние цифры Year. monthCode(Month, m(_M, C), _), !, % Получаем код месяца. S= Y+Y div 4+C+MD, R= S mod 7, /* День недели как число между 0 (воскресенье) и 6 (суббота) */ dayOfWeek(R, D). % Получаем функциональный символ дня calculateDay(_, _, _, err). run():- console::init(), calculateDay("May", 3, 2005, R), stdio::write("Day of the week: ", R), stdio::nl, succeed(). % place your own code here end implement func goal mainExe::run(func::run).
62
Глава 10: Как решать это в Прологе Заголовок этой главы – дань уважению Гельдеру Коэльо (Helder Coelho), первому автору лучшей книги по Прологу из когда‐либо написанных: How To Solve It With Prolog. Эта книга была отпечатана в Laboratorio Nacional de Engenharia Civil, где Гельдер работал, будучи в Португалии. Позже, Коэльо опубликовал Prolog by Example – «Пролог в примерах» (см. [Coelho/Cotta]), которая тоже хороша, но не так, как его первая книга. Книга Коэльо – это коллекция коротких проблем, предложенных и решённых великими матема‐ тиками и компьютерными учёными. Всё это организовано наподобие FAQ1. Проблемы интересны, а решения блестящи. Что более важно, книга доставляет много веселья. Я надеюсь, что скоро мы уви‐ дим новое издание, так как она показывает историю создателей логического программирования, их открытия, их исследования. Все программы в этой главе будут консольными приложениями. Кроме того, я буду приводить листинги определения основного класса. Все остальное остаётся за вами. Например, если я дам вам листинг implement hello open core clauses classInfo("hello", "1.0"). clauses run():- console::init(), stdio::write("Hello, world!\n"). end implement hello goal mainExe::run(hello::run).
Вы должны создать консольный проект, с названием hello.
10.1.
Утилиты
Устное утверждение: найти все элементы списка L. Логическая программа: %File: utilites.pro implement utilities open core class predicates member:(integer, integer_list) nondeterm anyflow. member:(string, string_list) nondeterm anyflow. test:(string_list) procedure (i). test:(integer_list) procedure (i). clauses classInfo("utilities", "1.0"). member(H, [H|_]). member(H, [_|T]) :- member(H, T). test(L) :- member(H, L), stdio::write(H), stdio::nl, fail or succeed(). test(_L). run():- console::init(), L= [2,3,4,5], test(L)
1
FAQ – Frequently Asked Questions. Вообще‐то, термин имеет вполне вменяемый перевод («часто задаваемые вопросы», ЧаВо) но в современных текстах почти везде употребляется оригинальное трёхбуквенное сочетание. Я тоже не буду переводить/расшифровывать этот термин.
63
S= [“a”, “b”, “c”], test(S). end implement utilities goal mainExe::run(utilities::run).
Пример показывает, что вы можете определить предикат с разными доменами. Например, там есть объявление member/2 для string_list1, и другое для integer_list. То же самое с test/1. Определение test/1 использует предикат fail для отката, и печатает все решения nondeterm предиката member. Как результат, программа печатает все элементы списка. Устное утверждение: проверить, является ли U пересечением списков L1 и L2; найти пересече‐ ние списков L1 и L2. implement intersection open core, stdio class predicates isec:(integer_list, integer_list, integer_list) nondeterm anyFlow. memb:(integer, integer_list) nondeterm anyFlow. test:(integer_list, integer_list, integer_list, string) procedure (i, i, i, o). findsec:(integer_list, integer_list, integer_list) procedure (i, i, o). length:(integer_list, integer) procedure (i, o). clauses classInfo("intersection", "1.0"). memb(H, [H1|T]) :- H=H1; memb(H, T). isec(L1, L2, [H|U]) :- memb(H, L1), memb(H, L2), !, isec(L1, L2, U). isec(_, _, []). length([], 0) :- !. length([_|R], L+1) :- length(R, L). test(L1, L2, U, R) :- findsec(L1, L2, S), length(U, LU), length(S, LS), LU= LS, isec(L1, L2, U), !, R= "yes"; R= "no". findsec([H|T], L, [H|U]) :- memb(H, L), !, findsec(T, L, U). findsec([_|T], L, U) :- findsec(T, L, U), !. findsec(_L1, _L2, []). run():- console::init(), L1= [3, 6, 4, 5], L2= [4, 5, 6], U1= [4, 6, 5], U2= [4, 3], test(L1, L2, U2, Resp2), test(L1, L2, U1, Resp1), write(Resp1, ", ", Resp2), nl, findsec(L1, L2, I), write(I), nl. end implement intersection goal mainExe::run(intersection::run).
Устное утверждение: определить отношение append/3 между тремя списками, которое со‐ храняется, если последний является результатом присоединения второго списка к первому. implement append open core class predicates app:(Elem* L1, Elem* L2, Elem* L3)
1
string_list, равно как и integer_list, являются встроенными в Visual Prolog доменами, полностью идентичными string* и integer* соответственно. За подробностями обращайтесь в справку Visual Prolog IDE.
64
nondeterm anyFlow. test1:(string*) procedure (i). test2:(integer_list, integer_list) procedure (i, i). clauses classInfo("append", "1.0"). app([], L, L). app([H|T], L, [H|U]) :- app(T, L, U). test1(U) :- app(L1, L2, U), stdio::write(L1, " ++ ", L2, "= ", U), stdio::nl, fail. test1(_). test2(L1, L2) :- app(L1, L2, U), stdio::write(L1, " ++ ", L2, ": ", U), stdio::nl, fail. test2(_, _). clauses run():- console::init(), test1(["a", "b", "c", "d"]), stdio::nl, test2([1, 2, 3], [4, 5, 6]). end implement append goal mainExe::run(append::run).
Эта проблема – фаворит всех времён. Если вы запустите программу, test1/1 найдёт и распеча‐ тает все способы разделения списка [2, 3, 4, 5]. С другой стороны, test2/2 соединяет два спи‐ ска. Оба предиката используют app/3, который производит две операции, конкатенацию и деление списков. Устное утверждение: написать программу, инвертирующую список [разворачивающую его по‐ рядок в обратную сторону – прим. пер.]. implement reverse open core clauses classInfo("reverse", "1.0"). class predicates rev:(integer_list, integer_list) procedure (i, o). rev1:(integer_list, integer_list, integer_list) procedure (i, i, o). clauses rev1([], L, L) :- !. rev1([H|T], L1, L) :- rev1(T, [H|L1], L). rev(L, R) :- rev1(L, [], R). run():- console::init(), L= [1, 2, 3, 4, 5], rev(L, R), stdio::write("Reverse of", L, "= ", R), stdio::nl. end implement reverse goal mainExe::run(reverse::run).
Эта программа полезна и блестяща. Она блестяща, потому что показывает, как использовать на‐ копляющие параметры очень эффективным образом. Предположим, что вы зададите Прологу цель rev([1, 2, 3], R)
Тогда, inference engine – механизм вывода попытается сделать rev([1, 2, 3], R) равным го‐ лове одного из предложений Хорна, находящихся в программе. В настоящем случае, он преуспеет, если сделает L=[1, 2, 3] в предложении rev(L, R) :- rev1(L, [], R)
65
Но, если L=[1, 2, 3], это предложение становится rev([1,2,3], R) :- rev1([1,2,3], [], R)
Этот процесс сопоставления цели и головы правила называется unification – унификацией, и можно сказать, что rev([1,2,3], R) унифицируется с rev(L, R) :- rev1(L, [], R), для L=[1,2,3]
производя rev1(L, [], R), который равен rev1([1,2,3], [], R), так как L=[1,2,3]. Ре‐ зультат унификации является хвостом Хорнова предложения, потому что вы должны доказать хвост, для того, чтобы доказать заголовок. Например, рассмотрим следующее Хорново предложение в за‐ писи по‐русски: X дедушка Y если X отец Z и Z отец Y.
Если вы хотите доказать, что X дедушка Y, вы должны доказать, что Y отец Z, и Z отец Y. Короче, нужно доказать хвост, чтобы доказать голову предложения Хорна. Вся эта долгая дискуссия может быть суммирована так: • rev([1,2,3], R) унифицируется с rev(L, R) :- rev1(L, [], R), для L=[1,2,3] выдавая rev1([1,2,3], [], R). • rev1([1,2,3], [ ], R) унифицируется с • • •
rev1([H|T],L1,L) :- rev1(T,[H|L1],L)/ H=1, L1=[], T=[2,3] производя rev1([2, 3], [1], L). rev1([2, 3], [1], L) унифицируется с rev1([H|T],L1,L) :- rev1(T,[H|L1],L)/ H=2, L1=[1], T=[3] выдавая rev1([3], [2,1], L). rev1([3], [2,1], L) унифицируется с rev1([H|T],L1,L) :- rev1(T,[H|L1],L)/ H=3, L1=[2,1], T=[] выдавая rev1([], [3,2,1], L). rev1([], [3,2,1], L) унифицируется с rev1([], L, L) :- ! для L=[3,2,1], производя результат L=[3,2,1].
Лаплас говорил, что Ньютон был не только величайшим учёным всех времён и народов, но и ещё очень удачливым человеком, за то, что он был рождён до Лапласа. Конечно же, Le Marquis мог бы открыть Небесную Механику до Ньютона, если бы имел возможность это сделать. Я полагаю, что вы написали бы быстрый алгоритм сортировки до Ван Эмдена (Van Emden), если бы он не был достаточ‐ но удачлив для того, чтобы положить руку на компилятор Пролога до вас. Быстрый алгоритм сорти‐ ровки был изобретён Тони Хоаре (Tony Hoare), который нуждался в быстром методе сортировки спи‐ сков и векторов. Это было время, когда B+Trees не существовало, и единственный способ совершить быстрый поиск в списке клиентов был иметь его сортированным. Давайте посмотрим, как Ван Эмден объяснил его алгоритм своему другу Коэльо. Прежде всего, он реализовал алгоритм split/4, который делит список L на два подсписка: S (от Small – малень‐ кий) и В (от Big ‐ большой). Подсписок S содержит все L, которые меньше или равны P (от Pivot – точ‐ ка опоры). Подсписок B содержит элементы, которые больше P. Вот как Ван Эмден реализовал split/4: implement quicksort open core clauses classInfo("quicksort", "1.0"). class predicates split:(integer, integer_list, integer_list, integer_list) procedure (i, i, o, o). clauses split(P, [H|T], S, [H|B]) :- H>P, !, split(P, T, S, B).
66
split(P, [H|T], [H|S], B) :- !, split(P, T, S, B). split(_, [], [], []). run(): console::init(), split(5, [3, 7, 4, 5, 8, 2], Small, Big), stdio::write("Small=", Small, ", Big= ", Big), stdio::nl. end implement quicksort goal mainExe::run(quicksort::run).
Если вы запустите эту программу, компьютер напечатает Small=[3,4,5,2], Big=[7,8]
что он и должен сделать. Теперь, давайте осознаем идею, стоящую за алгоритмом быстрой сор‐ тировки. Предположим, что у вас есть список [5, 3, 7, 4, 5, 8, 2], и вы хотите отсортировать его. • Берём первый элемент L=[5, 3, 7, 4, 5, 8, 2] как опорную точку и делим его на Small=[3,4,5,2] и Big=[7,8]. • Сортируем Small, используя тот же алгоритм; вы получите Sorted_small=[2, 3, 4, 5]
• •
Сортируем Big и получаем Sorted_Big=[7, 8]. Присоединяем Sorted_small к [Pivot|Sorted_Big]. Вы получите сортированный список: [2, 3, 4, 5, 5, 7, 8]. Этот алгоритм требует предикат, присоединяющий один список к другому. Вы можете использо‐ вать app/3, которому научились ранее на предыдущих страницах. Однако, тот алгоритм является nondeterm, что нам не нужно, так как нам не нужна функциональность деления списка на два; всё, что нам надо – чистая конкатенация. Таким образом, вы можете вставить отсечение после первого пред‐ ложения app/3. app([], L, L) :- !. app([H|T], L, [H|U]) :- app(T, L, U).
Объявите app как процедуру с двумя входными аргументами и одним выходным. app:( integer_list, integer_list, integer_list) procedure (i, i, o).
Чтобы проверить алгоритм, мы собираемся использовать список целых чисел. Конечно же, он будет более полезен со списком строк. implement quicksort open core clauses classInfo("quicksort", "1.0"). class predicates split:(E, E*, E*, E*) procedure (i, i, o, o). app:(E*, E*, E*) procedure (i, i, o). sort:(E*, E*) procedure (i, o). clauses split(P, [H|T], S, [H|B]) :- H>P, !, split(P, T, S, B). split(P, [H|T], [H|S], B) :- !, split(P, T, S, B). split(_, [], [], []). app([], L, L) :- !. app([H|T], L, [H|U]) :- app(T, L, U). sort([P|L], S) :- !, split(P, L, Small, Big), sort(Small, QSmall), sort(Big, QBig), app(QSmall, [P|QBig], S). sort([], []).
67
run():- console::init(), L= [5, 3, 7, 4, 5, 8, 2], sort(L, S), stdio::write("L=", L , ", S= ", S), stdio::nl, Strs= ["b", "a", "c"], sort(Strs, Qs), stdio::write("Qs= ", Qs), stdio::nl. end implement quicksort goal mainExe::run(quicksort::run).
Пролог был изобретён Аланом Колмероэ (Alain Colmerauer), французским лингвистом. Его сту‐ дент Филипп Руссел (Philippe Roussel) реализовал интерпретатор. Похоже, что жена Руссела дала язы‐ ку программирования имя Пролог. Уоррен (Warren) был человеком, реализовавшим компилятор. Приведённая ниже проблема была предложена этим самым Уорреном. Устное утверждение: количество дней в году 366 каждые четыре года; иначе оно 365. Сколько дней в 1975, 1976 и 2000 годах? implement numdays open core class predicates no_of_days_in:(integer, integer) procedure (i, o). member:(integer, integer_list) nondeterm (o, i). test:(integer_list) procedure (i). clauses classInfo("numdays", "1.0"). member(H, [H|_T]). member(H, [_|T]) :- member(H, T). no_of_days_in(Y, 366) :- 0= Y mod 4, not(0= Y mod 100), 0= Y mod 400, !. no_of_days_in(_Y, 365). test(L) :- member(X, L), no_of_days_in(X, N), stdio::write("Year ", X, " has ", N, " days."), stdio::nl, fail. test(_L). run(): console::init(), test([1975, 1976, 2000]). end implement numdays goal mainExe::run(numdays::run).
Луис Мониш Перейра (Luis Moniz Pereira) – португалец, работающий с искусственным интеллек‐ том. Он был одним из первых, использовавших Пролог для искусственного интеллекта. Он соавтор книги How to Solve it in Prolog. Устное утверждение: написать программу для раскрашивания любой плоской карты как мак‐ симум четырьмя цветами, так, что два смежных региона не будут иметь один цвет. Проблема является классической программой создания и проверки. В test(L), вызовы преди‐ катов generateColor(A), generateColor(B), generateColor(C), generateColor(D), generateColor(E), generateColor(F),
создают цвета для шести регионов карты. Затем, test(L) строит карту в виде списка соседст‐ вующих стран: L= [nb(A, B), nb(A, C), nb(A, E), nb(A, F),
68
nb(B, C), nb(B, D), nb(B, E), nb(B, F), nb(C, D), nb(C, F), nb(C, F)],
Наконец, предикат aMap(L) проверяет, является ли L допустимой картой. Она будет допусти‐ мой, если не будет двух соседствующих стран, окрашенных одним цветом. Если предикат aMap(L) проваливается, предложенные цвета не верны. Следовательно, программа откатывается к generateColor(X) для получения нового набора цветов. Проблема четырёх цветов интересна по двум причинам. 1. Схема создания и проверки была одной из первых техник искусственного интеллекта. 2. Есть очень известная теорема, которая утверждает: Каждая карта может быть раскрашена четырьмя цветами так, что прилегающим странам будут присвоены различные цвета.
Эта теорема была доказана в 1976 году Кеннетом Эппелом и Вольфгангом Хакеном (Kenneth Ap pel и Wolfgang Haken), двумя математиками в университете Иллинойса. implement fourcolors open core domains colors= blue;yellow;red;green. neighbors= nb(colors, colors). map= neighbors*. class predicates aMap:(map) nondeterm anyFlow. test:(map) procedure anyFlow. generateColor:(colors) multi (o). clauses classInfo("fourcolors", "1.0"). generateColor(R) :- R= blue; R= yellow; R= green; R= red. aMap([]). aMap([X|Xs]) :- X= nb(C1, C2), not(C1 = C2), aMap(Xs). test(L) :- generateColor(A), generateColor(B), generateColor(C), generateColor(D), generateColor(E), generateColor(F), L= [ nb(A, B), nb(A, C), nb(A, E), nb(A, F), nb(B, C), nb(B, D), nb(B, E), nb(B, F), nb(C, D), nb(C, F), nb(C, F)], aMap(L), !; L= []. run():- console::init(), test(L), stdio::write(L), stdio::nl. end implement fourcolors goal mainExe::run(fourcolors::run).
Устное утверждение: написать программу для игры в Ним (Nim). Состояние в игре Ним может быть представлено как набор кучек спичек; мы будем отображать каждую кучку как список целых чисел. [1, 1, 1, 1, 1]
Следовательно, множество кучек будет списком списков; например, [ [1,1,1,1,1], [1, 1],
69
[1,1,1,1] ]
Два игрока, вы и компьютер, по очереди делаете ходы. Как только игрок не сможет сделать ход, игра заканчивается, и этот игрок проигрывает. Ход состоит из взятия как минимум одной спички из ровно одной кучки. В примере, если вы возьмёте три спички из третьей кучки, вы сделаете допусти‐ мый ход, и доска станет [ [1,1,1,1,1], [1, 1], [1] ]
где вы убрали три спички из третьей кучки. Чтобы реализовать этот проект, мы будем использо‐ вать технику программирования, называемую incremental development of systems – наращивающая разработка систем. Сначала, вы реализуете и протестируете программу, которая присоединяет два списка. Так как вы собираетесь использовать эту программу и для разделения, и для конкатенации списков, вам нужно протестировать обе возможности. Фактически, если вы запустите первую про‐ грамму, из указанных (ниже), вы получите [[1],[1],[1],[1],[1],[1],[1],[1]] [][[1],[1],[1,1]] [[1]][[1],[1,1]] [[1],[1]][[1,1]] [[1],[1],[1,1]][]
Первая строка является результатом конкатенации двух множеств кучек; последующие строки показывают все возможности разделения списка кучек. Тогда первая программа, похоже, работает верно. implement nimgame open core domains ms= integer*. hs= ms*. class predicates append:(E*, E*, E*) nondeterm anyFlow. clauses classInfo( "nimgame", "1.0"). append([], Y, Y). append([U|X], Y, [U|Z]) :- append(X, Y, Z). clauses run():console::init(), append([[1],[1],[1],[1],[1]], [[1],[1],[1]], L), !, stdio::write(L), stdio::nl; succeed(). end implement nimgame goal mainExe::run(nimgame::run).
Следующий шаг – написать предикат, который забирает как минимум одну спичку из кучки; ко‐ нечно, он может забрать больше, чем одну спичку. После удаления одной или более спичек из кучки, takesome/3 вставляет измененную кучку во множество кучек. Если вы протестируете программу, она напечатает следующий результат: [[1,1,1,1],[1,1]] [[1,1,1],[1,1]] [[1,1],[1,1]] [[1],[1,1]] [[1,1]]
70
NB: первое предложение takesome/3 заверяет, что предикат не вставит пустую кучку во множе‐ ство кучек. implement nimgame open core domains ms= integer*. hs= ms*. class predicates append:(E*, E*, E*) nondeterm anyFlow. test:() procedure. takesome:(ms, hs, hs) nondeterm anyFlow. clauses classInfo( "nimgame", "1.0"). append([], Y, Y). append([U|X], Y, [U|Z]) :- append(X, Y, Z). takesome([_X], V, V). takesome([_X, X1|Y], V, [[X1|Y]|V]). takesome([_X|T], V, Y) :- takesome(T, V, Y). test() :- L= [1, 1, 1, 1, 1], V= [[1,1]], takesome(L, V, Resp), stdio::write(Resp), stdio::nl, fail; succeed(). clauses run(): console::init(), test(). end implement nimgame goal mainExe::run(nimgame::run).
Теперь вы готовы создавать все возможные ходы с заданной позиции. Если вы запустите тесто‐ вую программу, она выдаст следующий экран: Possible moves from [[1,1,1],[1,1]]: [[1,1],[1,1]] [[1],[1,1]] [[1,1]] [[1,1,1],[1]] [[1,1,1]] implement nimgame open core domains ms= integer*. hs= ms*. class predicates append:(E*, E*, E*) nondeterm anyFlow. takesome:(ms, hs, hs) nondeterm anyFlow. move:(hs, hs) nondeterm anyFlow. clauses classInfo( "nimgame", "1.0"). append([], Y, Y). append([U|X], Y, [U|Z]) :- append(X, Y, Z). takesome([_X], V, V). takesome([_X, X1|Y], V, [[X1|Y]|V]). takesome([_X|T], V, Y) :- takesome(T, V, Y). move(X, Y) :- append(U, [X1|V], X), takesome(X1, V, R), append(U, R, Y). run():- console::init(), L= [[1, 1, 1], [1, 1]], stdio::write("Possible moves from ", L, ": "), stdio::nl, move(L, Resp),
71
stdio::write(Resp), stdio::nl, fail; succeed(). end implement nimgame goal mainExe::run(nimgame::run).
implement nimgame open core domains ms= integer*. hs= ms*. class predicates append:(E*, E*, E*) nondeterm anyFlow. takesome:(ms, hs, hs) nondeterm anyFlow. move:(hs, hs) nondeterm anyFlow. winningMove:(hs, hs) nondeterm (i, o). clauses classInfo( "nimgame", "1.0"). append([], Y, Y). append([U|X], Y, [U|Z]) :- append(X, Y, Z). takesome([_X], V, V). takesome([_X, X1|Y], V, [[X1|Y]|V]). takesome([_X|T], V, Y) :- takesome(T, V, Y). move(X, Y) :- append(U, [X1|V], X), takesome(X1, V, R), append(U, R, Y). winningMove(X, Y) :- move(X, Y), not(winningMove(Y, _)). run():- console::init(), L= [[1, 1, 1], [1, 1]], winningMove(L, S), stdio::write("Winning move: ", S), stdio::nl, fail; succeed(). end implement nimgame goal mainExe::run(nimgame::run).
Сердце этой программы – предикат winningMove(X, Y) :- move(X, Y), not(winningMove(Y, _)).
Если есть выигрышная стратегия, этот потрясающий предикат в одну строчку найдёт её! Ниже вы найдёте пример игры против компьютера. Как вы можете видеть, если есть шанс выиг‐ рать, машина возьмёт его. [[1,1,1],[1,1]] Your move: [[1], [1,1]] My move: [[1],[1]] Your move: [[1]] My move: [] I win
Чтобы реализовать конечную программу, вы создадите класс для самой игры. Пусть названием для этого класса будет nim. Затем, основная программа дана ниже. % File: nimgame.pro implement nimgame open core, stdio clauses classInfo( "nimgame", "1.0"). run():- console::init(), L= [[1, 1, 1], [1, 1]], write(L), nl, nim::game(L), ! ; succeed(). end implement nimgame goal mainExe::run(nimgame::run).
72
Интерфейс класса экспортирует метод game/1 и домены, которые вам нужны для описания по‐ зиции в игре. Он определён ниже. % File: nim.cl class nim open core domains ms= integer*. hs= ms*. predicates classInfo : core::classInfo. game:(hs) determ (i). end class nim
% File:nim.pro implement nim open core, stdio class predicates append:(E*, E*, E*) nondeterm anyFlow. takesome:(ms, hs, hs) nondeterm anyFlow. move:(hs, hs) nondeterm anyFlow. winningMove:(hs, hs) nondeterm (i, o). iwin:(hs) determ (i). youwin:(hs, hs) determ (i, o). clauses classInfo("nim", "1.0"). append([], Y, Y). append([U|X], Y, [U|Z]) :- append(X, Y, Z). takesome([_X], V, V). takesome([_X, X1|Y], V, [[X1|Y]|V]). takesome([_X|T], V, Y) :- takesome(T, V, Y). move(X, Y) :- append(U, [X1|V], X), takesome(X1, V, R), append(U, R, Y). winningMove(X, Y) :- move(X, Y), not(winningMove(Y, _)). iwin([]) :- !, write("I win"), nl. youwin(L, L2) :- winningMove(L, L2), !. youwin(_L, _L2) :- write("You win"), nl, fail. game(L1) :- write("Your move: "), L= console::read(), move(L1, LTest), L= LTest, youwin(L, L2), !, write("My move: "), write(L2), nl, not(iwin(L2)), game(L2). end implement nim
Стратегия, использованная в этой очень простой программе, называется алгоритмом минимакса (minmax algorithm). Алгоритм может быть применён к любой игре с двумя соперниками, где оба имеют совершенно точное знание о состоянии игры. Примеры таких игр: шахматы, Го, шашки,, кре‐ стики‐нолики, и Ним. Он также может быть использован для контролирования роботов; фактически, это разновидность фильтра, основанного на стратегии минимакса , похожая на фильтр Калмана (Kal man).
73
В стратегии минимакса, состояния игры организованы в дерево, которое называется деревом поиска. Когда наступает ход компьютера, он создаёт состояния с минимальным весом для оппонента. С другой стороны, он пытается следовать веткам дерева, которые ведут к выигрышному положению. В простой игре вроде Ним1, компьютер способен найти путь к уверенной победе. В бо‐ лее сложных играх, как шахматы, это не всегда возможно; требуется прерывать поиск до достижения уверенности в победе. В этом случае, игровые стратеги разрабатывают функции, которые игровым позициям присваивают определённые веса, даже если эти позиции не являются выигрышными или проигрышными.
1
Данная программа может найти выигрышный ход только для небольшого количества спичек. С точки зрения математики задача поиска выигрышной стратегии для игры Ним далеко не тривиальна. Стратегия игры для общего случая описана в [Sterling and Shapiro]
74
Глава 11: Факты Фактом является хорновское предложение без хвоста. Факты могут быть добавлены, изменены или удалены1 динамически, во время исполнения программы. Пример поможет вам понять, почему факты в Прологе необходимы. Давайте предположим, что вы хотите создать маленькую базу данных публикаций. Когда вы получаете новую книгу, вы хотите добавить её к базе данных. addItem(journal("AI in focus", "MIT Press")),
Предикат addItem/1 может быть определён так: class predicates addItem:(volume). clauses addItem(V) : num := num+1, assert(item(V, num)).
Выполнение addItem(journal("AI", "AldinePress")) добавляет предложение item(journal("AI", "AldinePress")).
к базе данных, и увеличивает переменную num. Объявление class facts - bib num:integer := 0. item:(volume, integer) nondeterm.
создаёт базу данных фактов (fact database) bib, с переменной num и предикатом item/2; преди‐ кат nondeterm, а переменная single. Домен volume определён как domains name= string. author= n1(name); n2(name, name); etal(name). publisher= string. title= string. volume= journal(title, publisher); book(title, author).
Программа ниже показывает, как определять факты и как сохранять базу данных фактов в файл. % File facttest.pro implement facttest open core constants className = "facttest". classVersion = "". domains name= string. author= n1(name); n2(name, name); etal(name). publisher= string. title= string. volume= journal(title, publisher); book(title, author). class facts - bib num:integer := 0.
Несмотря на то, что используются не глаголы to add и to delete, а именно to assert и to retract, что наводит на размышления о том, что переводом должны были быть «утверждены» и «отменены», я решил использовать семантически верный перевод этих терминов, и буду использовать в дальнейшем именно его. 1
75
item:(volume, integer) nondeterm. class predicates addItem:(volume). prtDataBase:(). clauses classInfo(className, classVersion). addItem(V) :num := num+1, assert(item(V, num)). prtDataBase() :item(V, I), stdio::write(I, "=", V), stdio::nl, fail. prtDataBase(). clauses run():console::init(), addItem(journal("AI in focus", "MIT Press")), addItem(book("Databases in Prolog", n1("Wellesley Barros"))), file::save("bibliography.fac", bib), prtDataBase(), succeed(). % place your own code here end implement facttest goal mainExe::run(facttest::run).
После создания базы данных и сохранения её в файл, вы можете использовать её в другой про‐ грамме. Для проверки этой возможности, создайте консольный проект factread, и добавьте в него следующую программу: % File: factread.pro implement factread open core constants className = "factread". classVersion = "". domains name= string. author= n1(name); n2(name, name); etal(name). publisher= string. title= string. volume= journal(title, publisher); book(title, author). class facts - bib num:integer := 0. item:(volume, integer) nondeterm. class predicates prtDataBase:(). clauses classInfo(className, classVersion). prtDataBase() :item(V, I), stdio::write(I, "=", V), stdio::nl, fail. prtDataBase(). clauses run():console::init(), file::consult("bibliography.fac", bib), prtDataBase(), succeed(). % place your own code here end implement factread goal mainExe::run(factread::run).
76
Вам надо переместить файл bibliography.fac
созданный facttest, в factread/exe (папку /exe – прим. пер.). Затем, запустите программу factread.exe
Вы увидите, что программа прочтёт базу данных и распечатает её.
11.1.
Класс file
Вы использовали класс file для сохранения фактов в файл. В классе file есть много других полез‐ ных предикатов, которые вы изучите в этом разделе.
11.1.1. Чтение и вывод строки Вам часто требуется читать текстовый файл, делать что‐то с ним, и записывать его обратно. В прошлом, когда компьютеры не имели много памяти, было необходимо читать потоки символов. Сегодня даже ёмкость памяти персональных компьютеров исчисляется гигабайтами. Следовательно, лучший подход к решению проблемы это прочитать весь файл в строку, и использовать мощный класс string для её обработки. Пролог имеет два предиката для подобной работы, чтобы знали, readString:(string FileName, boolean IsUnicodeFile) -> string String procedure (i,o). writeString:(string Filename, string Source, boolean IsUnicodeFile) procedure (i,i,i).
Оба предиката имеют версии, не требующие указания, является ли файл Unicode. readString:(string FileName) -> string String procedure (i). writeString:(string Filename, string Source) procedure (i,i).
Рис. 11.1 показывает пример использования этих предикатов. В примере, прочитывается строка из файла, конвертируется в заглавные буквы и записывается в версии в заглавных буквах в другой файл. Я верю, что вы сможете разобрать функциональность класса file из справки. Таким образом, я не буду задерживаться на этой теме.
77
%File: filetest.pro implement filetest open core constants className = "filetest". classVersion = "". clauses classInfo(className, classVersion). clauses run(): console::init(), Str= file::readString("test.txt", Unicode), stdio::write(Str), S_Upper= string::toUpperCase(Str), file::writeString("upper.txt", S_Upper, Unicode), succeed(). % place your own code here end implement filetest goal mainExe::run(filetest::run).
Рисунок 11.1 Чтение строки из файла.
78
Глава 12: Классы и объекты Давайте создадим исследовательский консольный проект, чтобы лучше понять, как классы и объекты работают. • Создайте новый консольный проект. Project Name: classexample UI Strategy: console
• •
Создайте новый класс: account. Не убирайте галочку Creates Objects. В этот раз вам ну‐ жен класс с объектами. Измените файл account.i как показано:
% File:account.i interface account open core predicates ssN:(string SocialSecurity) procedure (o). setSocialSecurity:(string SSN) procedure(i). deposit:(real). end interface account
Класс account создаст новый account объект, когда вы вызовете метод A=account::new(). Вы можете посылать сообщения в этот объект. Например, вы можете установить SocialSecurity кли‐ ента: A:setSocialSecurity("AA345"),
Впоследствии, вы можете извлечь этот самый Social Security. A:ssN(SocialSecurity),
И ssN/1 и setSocialSecurity/1 должны быть объявлены в интерфейсе, который в файле account.i. Банковскому счёту [собственно account – прим. пер.] требуются другие методы, кроме ssN/1 и setSocialSecurity/1. Например, вам могут понадобиться методы для депозитов, для паролей, и т.д. Я оставлю вашему воображению создание этих методов. Интересным дополнением могли бы быть методы криптографии. • Измените account.pro как показано ниже. % File: account.pro implement account open core facts - customer funds:real := 3.0. personalData:(string Name, string SocialSecurity) single. clauses classInfo("account", "1.0"). personalData("", ""). ssN(SS) :- personalData(_, SS). setSocialSecurity(SSN) :- personalData(N, _), assert(personalData( N, SSN)). deposit(Value) :- funds := funds+Value. end implement account
•
Измените run/0 в файле classexample.pro, для проверки нового класса.
79
run():- console::init(), A= account::new(), A:setSocialSecurity("AA345"), A:ssN(SocialSecurity), stdio::write(SocialSecurity), stdio::nl.
12.1.
Факты объектов
Объекты имеют факты, которые могут быть использованы для хранения их состояний. Философ Витгенштейн (Wittgenstein) очень любил факты и состояния вещей. Давайте посмотрим на первые несколько предложений его Tratactus LogicusPhilosophicus1. 1. Мир есть всё, что имеет место. (The World is everything that is the case2) В немецком, философ использовал слово Fall: Die Welt ist alles, was der Fall ist. Латинское слово casus означает падение, что объясняет перевод. Однако, что в мире падает? Игральные кости Судьбы падают. Есть множество возможных событий. Фортуна выбирает своими кубика‐ ми, что случится. Как минимум, это то, во что верили римляне. Помните, что Юлий Цезарь 3 сказал Alea jacta est ? Я знаю! Он сказал это по‐гречески, и Суэтоний (Suetonius) перевёл это на латынь, но смысл остался. • Мир есть совокупность фактов, а не вещей. = Die Welt ist die Gesamtheit der Tat sachen, nicht der Dinge. Вы заметили, как Витгенштейн любил факты? Позже, вы увидите, что он думал, что факты должны иметь состояния. • Мир распадается на факты. Отсюда, я избавлю вас от немецкого оригинала. • Мир определён фактами и тем, что это все факты. • Потому что совокупность всех фактов определяет как всё то, что имеет место, так и всё то, что не имеет места. Обратите внимание, что Витген‐ штейн рассматривал и факты, которые случились (то, что имеет место), и также те, что являются лишь возможностями (то, что не имеет место). 2. Что имеет место, является фактом, ‐ это существование атомарных фактов. • Атомарный факт есть соединение объектов. • Объект прост. • Объекты содержат возможность всех положений вещей . В Visual Prolog объ‐ ектные факты и переменные используются для отображения состояния объекта. • Возможность вхождения объекта в атомарные факты есть его форма. Можно увидеть, что идея классов и объектов – это старая идея в западной философии. То же верно и для состояний. В примере, объектами являются индивидуальные счета. Состояние счёта (на‐ шего объекта) определяет фонды, имя владельца счёта, , его/её Social Security Number – номер соци‐ ального страхования, пароль, и т.д. Это состояние отображается фактом и переменной: facts - customer funds:real := 3.0. personalData:(string Name,string SocialSecurity) single.
Предикаты интерфейса используются для доступа к фактам, которые выдают состояние объекта. Перевод основан на классическом переводе Tratactus Logicus Philosophicus на русский язык‐ Витгенштейн, Л. Логико‐философский трактат / Пер. с нем. Добронравова и Лахути Д.; Общ. ред. и предисл. Асмуса В. Ф. — М.: Наука, 1958. — 133 с. 2 Здесь весьма тонкий момент, вызывающий много споров среди переводчиков «Трактата». Мы перевели это так, как обычно переводится немецкая идиома “der Fall ist” и английская ”is the case”, которым перевели его Рамсей и Огден в 1922 г. – "имеет место". Но дело в том, что немецкое Fall само по себе может означать "случай, происшествие, падение"; английское case (происходящее от латинского casus – случай, казус) тоже означает “случай», поэтому некоторые переводчики переводят это предложение как «Мир есть все, что случается (или: происходит)», хотя мы (и другие) с этим не согласны. А английское fall не означает «случай», но означает «падение, падать». Автор играет разными смыслами немецкого и английского Fall/fall: «что падает в мире? Падают кости Фортуны», и т.д. – прим. Д.Г. Лахути. 3 Лат. Жребий брошен. 1
80
% File:account.i interface account open core predicates ssN:(string SocialSecurity) procedure (o). setSocialSecurity:(string SSN) procedure(i). deposit:(real). end interface account
81
Глава 13: Giuseppe Peano Джузеппе Пеано (Giuseppe Peano) был одним из величайших математиков всех времён. Он напи‐ сал маленькую книжку, чьим названием было Arithmetices Principia Novo Methodo Exposita
Уже отсюда вы можете понять, насколько он был велик. Не много людей пишут на латыни в это время и находят читателей. Гаусс (Gauss) был одним из этих немногих математиков, способных по‐ зволить себе писать на латыни. Пеано был ещё одним. Конечно же, Гаусс писал в основном на латы‐ ни. Пеано писал в основном на французском, языке науки начала прошлого века, и итальянском, языке его матери. Единственной вещью, написанной им на латинском языке, была Arithmetices Principia, очень короткая книга на 29 страницах. Там он предлагал миру современную форму записи для формальной логики – ветви математики, давшей начало Прологу. Он также говорил о других ве‐ щах, таких, как теория чисел и рекурсия. Я сильно советую вам выучить латынь и прочесть книгу Пеа‐ но.
13.1.
Черепашья графика
Перед погружением во вклады Пеано в науку (а там их много), давайте реализуем в Прологе че‐ репашью графику – turtle graphics. Создайте объектно‐ориентированный GUI со следующими на‐ стройками проекта: General Project name: peano UI Strategy: Object-oriented GUI (pfc/gui) Target type: Exe
• • •
• •
Создайте новую форму: canvas (раздел 2.1) Название формы canvas и она находится в корне проекта peano. Создайте класс под названием curve: отправляйтесь в окно проекта, выберите папку plotter, выберите пункт меню File/New и создайте класс. Не забудьте убрать Create Objects. Создайте пакет под названием turtleGraphics в каталоге C:\VIP7 и вставьте класс с названием turtle в него. Чтобы выполнить это, находясь в диалоговом окне Create Project Item, нажмите на кнопку Browse под Parent Directory, вы получите диалоговое окно Set New Directory (рис. 13.1), затем выберите каталог C:\vip7\. Не забудьте отключить Create Objects. Удостоверьтесь, что положили пакет turtleGraphics вне проекта peano. Так будет легче многократно его использовать. Откомпилируйте программу. Включите пункт File/New в TaskMenu.mnu. Добавьте следующий код
clauses onFileNew(S, _MenuTag) : X= canvas::new(S), X:show().
•
к TaskWindow.win/Code Expert/Menu/TaskMenu/id_file/id_file_new. Отредактируйте curve.cl и curve.pro как показано на рис. 13.2. Отредактируйте turtle.cl и turtle.pro как показано на рис. 13.3 и 13.4. Откомпилируйте программу.
82
Рисунок 13.1 Диалоговое окно Set New Directory
•
Добавьте следующий код
clauses onPaint(S, _Rectangle, _GDIObject) : W= S:getVPIWindow(), curve::drawCurve(W).
к Project Tree/canvas.frm/Code Expert/Window/Paint>onPaint. • Отредактируйте файлы curve.cl и curve.pro как на рис. 13.2. Откомпилируйте и запустите проект. Когда вы выберете пункт File/New в меню приложения, ок‐ но со звездой появится на экране. Класс turtle реализует рисующую turtle – черепаху, как в Logo, языке, созданном Папертом (Pa pert) в попытке продолжить идеи Жана Пиажета (Jean Piaget). Черепаха – это объект, который дви‐ жется по окну, оставляя за собой след. Черепаха имеет две переменные состояния, то есть, её поло‐ жение и направление движения. Эти переменные состояния заданы фактом: class facts turtle:(pnt, real) single. clauses turtle(pnt(200, 200), -pi/2). …
83
% File: curve.cl class curve open core, vpiDomains predicates classInfo : core::classInfo. drawCurve:(windowHandle). end class curve %File: curve.pro implement curve open core, vpi, vpiDomains, math class predicates star:(windowHandle, integer, real, integer) procedure (i, i, i, i). clauses classInfo("plotter/curve", "1.0"). star(_Win, 0, _A, _L) :- !. star(Win, N, A, L) :turtle::right(A), turtle::forward(Win, L), star(Win, N-1, A, L). drawCurve(Win) :star(Win, 10, 3*pi/5, 40). end implement curve
Рисунок 13.2 Файлы curve.cl и curve.pro
Рассмотрим предикат: forward(Win, L) :turtle(P1, Facing), P1= pnt(X1, Y1), X2= round(X1+ L*cos(Facing)), Y2= round(Y1+L*sin(Facing)), P2= pnt(X2, Y2), drawline(Win, P1, P2), assert(turtle(P2, Facing)).
Он реализует движение черепахи вперёд. Если черепаха находится на позиции P1=pnt(X1, Y1) и движется на расстояние L в направлении Facing, можно найти её новую позицию, проецируя L на оси X и Y. Используя формулу Птолемея, получим: cos sin После получения новой позиции P2=pnt(X2, Y2), forward/2 использует drawline(Win, P1, P2)
для соединения точки P1 с P2. Наконец, информация о новом положении черепахи добавляется в базу данных: assert(turtle(P2, Facing))
Предикат move/1 похож на forward/2, но не соединяет новое положение черепахи со старым. Предикаты right/1 и left/1 поворачивают черепаху соответственно направо и налево. Их реали‐ зации просты.
84
% File: turtle.cl class turtle open core, vpiDomains predicates classInfo : core::classInfo. forward:(windowHandle, integer) procedure. move:(integer) procedure. right:(real) procedure. left:(real) procedure. end class turtle
Рисунок 13.3 Файл turtle.cl
13.2.
Рекурсия
Постулат рекурсии Пеано может быть сформулирован так: Для того, чтобы доказать, что пре‐ дикат истинен для любого натурального числа, • Докажите, что он истинен для 0. • Докажите, что он истинен для N, если он истинен для N‐1.
% File: turtle.pro implement turtle open core, vpiDomains, vpi, math class facts turtle:(pnt, real) single. clauses classInfo("turtleGraphics/turtle", "1.0"). turtle(pnt(80, 80), -pi/2). forward(Win, L) :turtle(P1, Facing), P1= pnt(X1, Y1), X2= round(X1+ L*cos(Facing)), Y2= round(Y1+L*sin(Facing)), P2= pnt(X2, Y2), drawline(Win, P1, P2), assert(turtle(P2, Facing)). move(L) :turtle(P1, Facing), P1= pnt(X1, Y1), X2= round(X1+ L*cos(Facing)), Y2= round(Y1+L*sin(Facing)), P2= pnt(X2, Y2), assert(turtle(P2, Facing)). right(A) :turtle(P1, Facing), NewAngle= Facing+A, assert(turtle(P1, NewAngle)). left(A) :turtle(P1, Facing), NewAngle= Facing-A, assert(turtle(P1, NewAngle)). end implement turtle
Рисунок 13.4 Файл turtle.pro
85
Конечно, эта версия постулата начинается с 0, и написана на английском. Оригинал начинался с ∞ и был на латыни. Мило, не правда ли? В любом случае, давайте рассмотрим предикат star/4 (см. рис. 13.2) в свете постулата Пеано. Первое предложение star(_Win, 0, _A, _L) :- !.
гласит: вы рисуете 0 точек к звезде, если вы ничего не делаете. Второе предложение star(Win, N, A, L) :- turtle::right(A), turtle::forward(Win, L), star(Win, N-1, A, L).
гласит: вы рисуете N точек к звезде, если вы рисуете точку (поворачиваетесь на A радиан направо и идёте L пикселей прямо), и переходите к рисованию остальных N‐1 точек.
13.3.
Кривая Пеано
Пеано разработал рекурсивную кривую, которая заполняет пространство, что математики рас‐ сматривают как очень интересное свойство кривых. Что я на самом деле хотел нарисовать, когда я создал проект peano, это была кривая Пеано, но тогда я решил начать с более лёгкого примера. В любом случае, чтобы получить кривую Пеано, всё, что вам надо сделать, это заменить curve.pro данный на рис. 13.2 файлом curve.pro данным на рис. 13.5.
implement curve open core, vpi, vpiDomains, math class predicates peano:(windowHandle, integer, real, integer) procedure (i, i, i, i). clauses classInfo("plotter/curve", "1.0"). peano(_Win, N, _A, _H) :- N<1, !. peano(Win, N, A, H) :turtle::right(A), peano(Win, N-1, -A, H), turtle::forward(Win, H), peano(Win, N-1, A, H), turtle::forward(Win, H), peano(Win, N-1, -A, H), turtle::left(A). drawCurve(Win) :turtle::move(-60), peano(Win, 6, pi/2, 5). end implement curve
Рисунок 13.5 Кривая Пеано
86
13.4.
Latino Sine Flexione1
Вы увидели, что Пеано создал современную форму записи для логики. Он также выдвинул идею рекурсии. Так как рекурсия является базовой схемой программирования в Прологе, вы можете ска‐ зать, что Пеано внёс два важных вклада в логическое программирование. Однако он на этом не оста‐ новился. Прежний Пролог был изобретён Колмероэ (Colmerauer) для обработки естественных языков: Язык программирования, Пролог, родился из проекта, нацеленного не на создание языка программирования, но на обработку естественных языков; в данном случае, французского Колмероэ. В случае естественных языков, Пеано сделал интересное предложение. Так как большинство ев‐ ропейских языков заимствовали большое число латинских слов, почему не использовать латынь для научного общения? Всё, что вам нужно – это выбрать латинские слова, обычные для главных евро‐ пейских языков, и вы получите словарь достаточно большой, чтобы выразить почти что любую идею. Проблема в том, что латынь – сложный язык, со всеми этими склонениями и словоформами. Пеано позаботился и об этом: из своей марки латинского языка он убрал все флексии 2. Давайте взглянем на несколько цитат из Key to Interlingua, где Interlingua – официальное название латыни без флексий. 1. Interlingua заимствует каждое слово, обычное для английского, немецкого, французского, испанского, итальянского, португальского, русского языков, и каждое англо‐латинское слово; 2. Каждое слово имеет форму латинской основы или корня. Словарь Interlingua собран из великого множества латинских слов, которые пришли в англий‐ ский, и, следовательно, легко понимаемых англоговорящим человеком. Он включает в себя все тер‐ мины, используемые в научной номенклатуре ботаники, химии, медицины, зоологии и других наук. Многие из этих терминов также греческие или греко‐латинские. Несколько классических латинских слов без международных эквивалентов являются частью словаря, а именно Latino Английский Русский Пример que that Что, который Interlingua adopta vocabulo que existe in Anglo, Germano, Franco et Russo. de of Из Latino habe praecisione de expressione. et and И et cetera, et al. sed but Но Vocabulario de Latino non es formato ad arbitrio sed consiste de vo‐ cabulos in usu in Anglo, Russo et Germano. isto this, that Это, что pro isto («потому что») illo he, it Он, оно Illo es Americano. illa she Она Illa es Americana me I, me Я Me programma in Prolog. te you Ты Te programma in Prolog. nos we Мы Nos programma in Prolog. vos you guys Вы Vos programma in Prolog. id it Это i.e., id est qui? who? Кто? Qui programma in Prolog? que? what, which Что, который Te programma in que? omne all Всё Illo es omnepotente= Он всемогущ. 1
Латинский без словоизменения Флексия (от лат. flexio – сгибание, изгиб) – показатель комплекса грамматических категорий, выражающихся в словоизменении; сама система словоизменения, пользующаяся такими показателями; то же, что окончание. – БСЭ, III издание, 1977 г. 2
87
Латинский не должен иметь никакой вид флексий. Множественное число формируется исполь‐ зованием слова plure, как в: Plure vocabulo in Latino es in usu in Anglo Латинские слова используются в английском.
Однако Пеано был демократом, и, так как большинство людей (англоговорящих людей – прим. пер.) хочет образовывать множественное число с s, он одобрил опциональное s‐множественное. Прилагательные могут быть созданы из существительных с помощью предлога de: • de fratre=fraternal – братский • de amico=friendly, amicable – дружеский Конечно же, есть множество слов, которые являются прилагательными от природы: internacio‐ nale, breve (short – короткий), magno (large, big ‐ большой), commune (common – обычный), и т.д. Вы можете использовать эти прилагательные как наречия, без изменений. Вы можете также образовать наречие используя слово modo: in modo de fratre (fraternally ‐ братски), in mode de patre (paternally ‐ отечески), in modo rapido (rapidly ‐ быстро). Я думаю, что этого достаточно. Вот пример текста на Lati no sine flexione: Interlingua es lingua universale que persona in modo facile scribe et intellige sine usu de speciale studio. Interlingua adopta vo cabulo que existe in Anglo, Germano, Franco et Russo. Usu de Interlingua es indicato pro scienti.co communicatione et pro com merciale correspondentia. Usu de Interlingua in internationale congressu facilita intelligentia et economiza tempore. In addi tione Interlingua es de valore educationale nam (because) illo es de logico et naturale formatione. Plure exemplo: Me habe uno capite, duo manu, duo pede.
Duo manu habe decem digito
Capite habe fronte, na so, ore, duo oculo, duo aure.
habe= иметь; habeas corpus означает: вы должны иметь тело. capite= голова; capital= головной город страны/навершие колонны. manu= рука; manual computation= вычисления от руки. pede= нога; pedal= рычаг, нажимаемый ногой. duo= два; duo= музыкальное произведение, исполняемое двумя певцами. decem = десять; decimal= основанный на числе десять; Рим‐ ляне имели обыкновение decimate любой легион, бежав‐ ший из битвы – то есть, они ставили всех в ряд, и убивали каждого десятого солдата. digito= палец; digital= сделанное пальцами fronte= лоб naso= нос; nasal= относящийся к носу. ore= рот; oral= произнесённый ртом. oculo= глаз; ocular inspection= осмотр, сделанный глазом. aure= ухо; aural= относящийся к уху.
Fronte es super naso et oculos. Naso es inter oculos. Dentes1 es in ore. Ore es sub naso. Oratore fac2 oratione orale per ore. Nos vide3 per oculos, audi4 per aures, loque1 per ore, ambula2 per pedes. 1 Dentes= зубы, как в dentist ‐ дантист. – прим. авт. 2 Fac= делать. Fact ‐ это что‐то сделанное. – прим. авт. 3 Nos vide= мы видим, как в videotape ‐ видеокассета. – прим. авт. 4 Audi= слышать, как в audit, audition ‐ прослушивание. – прим. авт.
88
Зачем нам нужен международный язык, как Interlingua Пеано, если у нас уже есть английский? Я думаю, что незачем. Однако, люди, предлагающие Interlingua, имеют несколько интересных аргу‐ ментов в защиту их проекта. Например, человечество не может позволить международному языку измениться, как только экономическая гегемония и культурный престиж сдвигается от одной нации к другой. В XIX веке французский был языком дипломатии. Затем, американская экономика превзошла французскую, и английский язык стал международным языком. В этом столетии Китай будет доми‐ нирующей суперсилой и будет навязывать письменный китайский другим нациям. Есть интересная история об этой точке зрения. Люди всё ещё используют латынь для описания растений. Так, если вы откроете новое растение, вы должны описать его на латинском. Это было верно и для зоологов и анатомов, но они перешли на английский не так давно. Поэтому, некоторые ботаники предложили последовать их примеру и принять современный язык и в ботанике. Проблема в том, что язык, кото‐ рый они хотят, китайский, не английский! Есть другой аргумент в пользу латыни. Языки вроде французского, английского и китайского сложны для изучения даже для людей, которые говорят на них как на родных. Не много французов могут писать на приемлемом французском. Очень немного американцев могут приблизиться к анг‐ лийскому письменному общению. Мой сын в восьмом классе средней школы Mount Logan, так я час‐ то хожу туда проверить его успехи. Большинство его одноклассников не могут читать, не говоря уже о том, чтобы писать. Если это происходит с американцами, что я могу сказать про японцев, которые пробуют учить английский? Подводя итоги: десяти лет недостаточно для того, чтобы научить амери‐ канца писать английскую прозу, но за десять дней мы сможем писать на приемлемой латыни. Давайте соберём проект, который берёт текст на LsF (Latino sine Flexione) и объясняет трудные слова и выражения. • Создайте новый GUI проект: LsF. • Добавьте новый пакет к дереву проекта: LsF/editor. • Создайте новую форму с меню: editor/txtWin. Выберите Task Menu в выпадающем списке menu в диалоговом окне Form Properties. Измените размер формы, потому что edi‐ torControl должен быть довольно большим. Добавьте нестандартный элемент управле‐ ния на форму. Нестандартный изображается иконкой ключа. Система предложит вам список названий нестандартных элементов управления; выберите editorControl. Из‐ мените размеры панели редактора.
• • • •
Build/Build приложение. Включите пункт File/New (раздел 2.2) Вставьте новый элемент в подменю Help из TaskMenu: Help/Grammar. Для назначения горячей клавиши, воспользуйтесь меню accelerator. Создайте класс editor/grammar с отключенным Creates Objects. Если вы не помните, как это делать, взгляните на раздел 4.6. Откомпилируйте приложение ещё раз. Измените файлы grammar.cl и grammar.pro как показано ниже
%File: grammar.cl class grammar open core predicates classInfo : core::classInfo. dic:(string).
1 Loque= говорить, как в loquacious ‐ болтливый. – прим. авт. 2 Ambula= ходить, как в ambulatory – амбулаторный/ходячий больной. – прим. авт.
89
end class grammar %File: grammar.pro implement grammar open core clauses classInfo( "editor/grammar", "1.0"). dic(S) :- stdio::write(S), stdio::nl. end implement grammar
•
Добавьте фрагмент
clauses onFileNew(S, _MenuTag) :- X= txtWin::new(S), X:show().
к ProjWin/TaskWindow.win/Code Expert/Menu/TaskMenu/id_file/id_file_new. •
Добавьте фрагмент
onHelpGrammar(_Source, _MenuTag) : editor_ctl:getSelection(Start, End), Txt=editor_ctl:getText(Start, End), grammar::dic(Txt).
• •
к ProjWin/txtWin/CodeExpert/Menu/TaskMenu/id_help/id_help_grammar. В том же файле, в который вы добавляли последний код для onHelpGrammar (txtWin.pro), измените определение предиката new как показано на рис. 13.6. Наконец, создайте файл LsF.txt в каталоге \exe с нижеприведённым текстом.
Interlingua es lingua universale que persona in modo facile scribe et intellige sine usu de speciale studio. Interlingua es indicato pro scientifico communicatione et pro commerciale correspondentia. Usu de Interlingua in internationale congressu economiza tempore.
Если вы откроете новое txtWin из приложения, вышеприведённый текст появится в окне. Если вы выделите часть текста, и выберете пункт Help/Grammar из TaskMenu приложения, вы отразите выде‐ ленный текст в message window – окне сообщений. Следующий шаг – добавить словарные вхождения в выбранный текст, до того, как писать его в окне сообщений (см. рис. 13.7). Мы начнём с разделения выделенного текста на список слов. На рис. 13.7 это сделано следующим предикатом: strTok(S, [T1|L]) :frontToken(S, T, Rest), % Получить первое слово из S. !, % Если успешно получили слово, отсечение! addExplanation(T, T1), % Добавляем объяснение к слову. strTok(Rest, L). % Анализируем остаток строки. strTok(_S, []).
Обработка естественных языков – наиболее сложная ветвь компьютерной науки, и выходит за рамки этого документа. Если вы хотите постигнуть это искусство, вы можете начать с добавления элементов в программу на рис. 13.7. Затем, поискать в интернете руководство о том, как писать на Прологе парсеры, и попытаться написать грамматику для LsF.
13.5.
Примеры
Примеры этой главы находятся в папках peano и LsF.
90
class predicates tryGetFileContent:( string FileName, string InputStr, string UpdatedFileName) procedure (i, o, o). clauses tryGetFileContent( "", "", "LsF.txt") :- !. tryGetFileContent(FileName, InputStr, FileName) : trap(file::existFile(FileName), TraceId, (exception::clear(TraceId), fail)), !, InputStr= file::readString(FileName, _). tryGetFileContent(FileName, "", FileName). new(Parent):- formWindow::new(Parent), generatedInitialize(), PossibleName= "LsF.txt", tryGetFileContent(PossibleName, InputStr, FileName), !, XX=string::concat("Here: ", InputStr), editor_ctl:pasteStr(XX). %editor_ctl:setInputString(InputStr).
Рисунок 13.6 Вставка файла в редактор
%File: grammar.pro implement grammar open core, string class predicates strTok:(string, string_list) procedure (i, o). addExplanation:(string, string) procedure (i, o). class facts entry:(string, string). clauses classInfo( "editor/grammar", "1.0"). entry("que", "that"). entry("sine", "without"). entry("es", "am, are, is, was, were, to be"). entry("de", "of"). entry("et", "and"). addExplanation(T, R) : entry(T, Meaning), !, R= format("%s= %s\n", T, Meaning). addExplanation(_T, ""). strTok(S, [T1|L]) :- frontToken(S, T, Rest), !, addExplanation(T, T1), strTok(Rest, L). strTok(_S, []). dic(S) : strTok(S, SList), Resp= concatList(SList), stdio::write(Resp), stdio::nl. end implement grammar Рисунок 13.7: Словарь «сложных» слов LsF
91
Глава 14: Lсистемы Аристид Линденмайер (Aristid Lindenmayer) – датский ботаник, который изобрёл умный способ описания форм растений. Как вы знаете, ботаники тратят большое количество времени, описывая форму растений. Их описания обычно делаются на упрощённой версии латыни, изобретённой Кар‐ лом Линнеем (Carl Linaelus). Линденмайеровский метод описания формы растений основывается на рисующей черепахе, ко‐ торую вы изучили в разделе 12.1. В интернете вы найдёте огромное количество материала по этой теме. Таким образом, я ограничу себя примером, который вы можете использовать как стартовую точку для ваших собственных проектов. • Создайте GUI проект: Lsys. • Добавьте пакет к корню дерева проекта: aristid. • Добавьте форму к пакету aristid: canvas. • Добавьте класс к aristid: draw. Снимите галочку Create Objects. Откомпилируйте про‐ ект, чтобы вставить класс в дерево проекта. • Включите пункт File/New панели задач. Добавьте clauses onFileNew(S, _MenuTag) : X= canvas::new(S), X:show().
к TaskWindow.win/CodeExpert/Menu/TaskMenu/id_file/id_file_new.
14.1.
Класс draw
Добавьте фрагмент clauses onPaint(S, _Rectangle, _GDIObject) : draw::tree(S:getVPIWindow()).
к ProjTree/canvas.frm/CodeExpert/Window/Paint. Затем, отредактируйте draw как показано ниже. Откомпилируйте и запустите проект. Откройте форму для рисования, выбрав пункт File/New из меню приложения. Она покажет дере‐ во, как на рисунке. %File: draw.cl class draw open core, vpiDomains predicates classInfo : core::classInfo. tree:(windowHandle). end class draw % File: draw.pro implement draw open core, vpiDomains, vpi, math clauses classInfo("plotter/draw", "1.0"). domains command = t(commandList); f(integer); r(integer); m. commandList= command*. class facts pos:(real Delta, real Turn) single.
92
grammar:(commandList) single. class predicates move:(windowHandle, pnt, real, commandList) procedure (i, i, i, i). derive:(commandList, commandList) procedure (i, o). mv:(windowHandle, pnt, real, command, pnt, real) procedure (i, i, i, i, o, o). iter:(integer, commandList, commandList) procedure (i, i, o). clauses pos(4.0, 0.1745329). grammar([f(1)]). iter(0, S, S) :- !. iter(I, InTree, OutTree) : derive(InTree, S), I1= I-1, iter(I1, S, OutTree). derive([], []) :- !. derive([f(0)|Rest], [f(0),f(0)|S]) :- !, derive(Rest, S). derive([f(_)|Rest], [f(0), t([r(1),f(1)]), f(0), t([r(-1),f(1)]), r(1), f(1)|S]) :- !, derive(Rest, S). derive([t(Branches)|Rest], [t(B)|S]) :- !, derive(Branches, B), derive(Rest, S). derive([X|Rest], [X|S]) : derive(Rest, S). mv(Win, P1, Facing, f(_), P2, Facing) :- !, pos(Displacement, _Turn), P1= pnt(X1, Y1), X2= round(X1+ Displacement*cos(Facing)), Y2= round(Y1+Displacement*sin(Facing)), P2= pnt(X2, Y2), drawline(Win, P1, P2). mv(_Win, P1, Facing, m, P2, Facing) :- !, pos(Displacement, _Turn), P1= pnt(X1, Y1), X2= round(X1+ Displacement*cos(Facing)), Y2= round(Y1+Displacement*sin(Facing)), P2= pnt(X2, Y2). mv(_Win, P1, Facing, r(Direction), P1, F) :- !, pos(_Displacement, Turn), F= Facing+ Direction*Turn. mv(Win, P1, Facing, t(B), P1, Facing) : move(Win, P1, Facing, B). move(_Win, _P1, _Facing, []) :- !. move(Win, P1, Facing, [X|Rest]) : mv(Win, P1, Facing, X, P, F), move(Win, P, F, Rest). tree(Win) : grammar(Commands), iter(5, Commands, C), Facing= -pi/2,
93
Point= pnt(100, 250), move(Win, Point, Facing, C). end implement draw
14.2.
Примеры
Пример этой главы – в папке Lsys.
94
Глава 15: Игры В этой главе вы научитесь реализовывать игры в Visual Prolog. Конечно же, хорошая игра это как хорошая поэма. Недостаточно выучить русский язык и правила стихосложения для того, чтобы писать как Пушкин. Поэт, кроме знания языка, на котором он будет писать, нуждается в воображении, креа‐ тивности, и т.д. То же самое относится к хорошему разработчику игр. Вам нужно много творчества для того, чтобы сделать что‐то наподобие Tetris или Space Invaders. Я использовал эти примеры для того, чтобы показать, что хорошие игры не зависят от прихотливой графики или звуковых эффектов. Важными особенностями являются тревожное ожидание, длительное напряжение и ощущение на‐ грады за хорошее выступление. Однажды, молодой человек спросил Лопе де Вега (Lope de Vega), как писать стихи. Ответ был: Это просто: Заглавная буква в начале каждой строфы и рифма в конце. «Что же мне вставить в середину?», спросил парень. В середину, hay que poner talento [нужно поместить талант – пер. с исп.], сказал Лопе де Вега. В этом уроке рассматривается несколько техник, таких как заглавные буквы и рифмы. Чтобы по‐ лучить хорошую игру, вам потребуется добавить талант к рецепту. Игра, которую вы реализуете, простая. Есть червяк, быстро ползущий по экрану. Игрок должен изменять направление его движения для того, чтобы он не врезался в стены и препятствия. Вам не‐ обходимо реализовать следующие элементы: • Окно, где червяк будет ползать. Окно должно иметь четыре кнопки для управления дви‐ жением червяка. • Класс состояния, который описывает направление движения и положение червя. Он экс‐ портирует предикаты mov/0, для изменения положения червя, и turn/2, который может быть использован для поворотов. • Класс, который рисует изображение положения червя. Он экспортирует предикат snapshot(Win). Обратите внимание на следующую деталь: предикат snapshot/1 не может нарисовать червя прямо на окне. Это бы заставило экран мерцать. Он должен построить изображение, и затем нарисовать его на экране. • Таймер, который вызывает mov/0 и snapshot/1 через фиксированные интервалы, для создания иллюзии движения. Мы знаем, что делать; давайте это делать. Я избегал создания объектов до этого момента. Фак‐ тически, каждый раз, когда вы создавали класс, вам рекомендовалось снимать галочку Creates Objects. Однако, если мы жестко будем следовать этой установке, наша игра будет иметь очень не‐ приятное свойство. Если вы проиграете, выйдете из текущей игры и попробуете начать новую, вы найдёте, что новая игра не в начальном состоянии. Фактически, она будет в состоянии, где вы проиг‐ рали старую. Проблема в том, что переменные, используемые для описания состояния, имеют па‐ мять, которая переходит от одной формы к другой. Объекты были изобретены для предотвращения этого рода неприятного поведения. Поэтому, давайте примем эту очень хорошую возможность изу‐ чить, как создавать объекты и работать с состояниями объектов. • Создайте проект: game. Project Name: game UI Strategy: Object-Oriented GUI (pfc/vpi) TargetType: Exe Base Directory: C\vip70 Sub-Directory: game
•
Создайте пакет: playground.
95
Рисунок 15.1 Форма лужайки
•
Создайте форму: lawn. Вы добавите четыре кнопки к форме lawn; обращайтесь к рис. 15.1 за оформлением. Хорошей идеей будет сменить названия кнопок в диалоговом окне Properties с pushButton_ctl на up_ctl, down_ctl, left_ctl, и right_ctl. Откомпи‐ лируйте приложение для включения в него формы. Затем включите File/New и добавьте
clauses onFileNew(S, _MenuTag) :- X= lawn::new(S), X:show().
•
• •
к ProjWin/Code Expert/Menu/TaskMenu/id_file/id_file_new. Создайте класс objstate внутри playground. Не отключайте Create Objects для класса objstate. Внесите в него код, показанный на рис. 15.3; NB: Класс objstate должен иметь интерфейс в файле objstate.i. Откомпилируйте приложение. Создайте класс draw внутри playground. Отключите Create Objects. Внесите код с рис. 15.2. Откомпилируйте приложение. Создайте класс click для хранения часов. Снимите Creates Objects.
%File: click.cl class click open core, vpiDomains predicates bip:(window). kill:(). end class click % File:click.pro implement click open core class facts tm:unsigned := 0. clauses bip(W) :- tm := W:timerSet(500). kill() :- vpi::timerKill(tm). end implement click
•
Откомпилируйте приложение для включения click в проект. Добавьте
clauses onDestroy(_Source) :- click::kill().
к ProjTree/lawn.frm/Window/Destroy>onDestroy.
96
•
Добавьте
class facts stt:objstate := objstate::new(). clauses onShow(Parent, _CreationData) :- stt := objstate::new(), stt:init(), click::bip(Parent).
•
к ProjWin/lawn.frm/Code Expert/Window/Show>onShow. Добавьте код к кнопкам:
clauses onDown(_S) = button::defaultAction() : stt:turn(0, 10).
к ProjTree/lawn.win/CodeExpert/Controls/down_ctl. clauses onLeft(_S) = button::defaultAction() : stt:turn(-10, 0).
к ProjTree/lawn.win/CodeExpert/Controls/left_ctl. clauses onRight(_S) = button::defaultAction() : stt:turn(10, 0).
к ProjTree/lawn.win/CodeExpert/Controls/right_ctl. clauses onUp(_S) = button::defaultAction() : stt:turn(0, -10).
•
к ProjTree/lawn.win/CodeExpert/Controls/up_ctl. Добавьте
clauses onTimer(_Source, _TimerID) :- stt:mov(), R= rct(10, 10, 210, 210), invalidate(R).
к ProjTree/lawn.frm/Miscellaneous/Timer>onTimer. Добавьте clauses onPaint(_Source, _Rectangle, GDIObject) : draw::snapshot(GDIObject, stt).
к ProjWin/lawn.frm/Code Expert/Window/Show>onShow. Откомпилируйте и запустите программу. После выбора пункта File/New, выскочит окно с червя‐ ком. Вы можете управлять червяком, используя четыре кнопки. Улучшение игры остаётся за вами. Вы можете добавить препятствия, например стены. Вы также можете положить маленькие яблоки для питания червя. Наконец, вы можете написать 3D игру. 3D игра на самом деле очень проста. Сегменты имеют три координаты, и вам нужно найти их проекции на плоскость, прежде чем рисовать. Если вы поищете информацию в интернете, вы определённо найдёте формулы для вычисления перспективной проекции точки. Алекс Соарес (Alex Soares) написал оригинальную игру, на которой я основал это руководство. Его игра была в 3D, в Visual Prolog 5.2, ис‐ пользуя DirectX, библиотеку, содержащую много ресурсов для 3D игр.
97
Давайте продолжим с нашим изучением предикатов рисования. Если вы рисуете фигуры на каж‐ дый тик часов, вы получите анимацию. Однако, если вы производите рисование прямо на видимом окне, анимация будет мигать, что не очень хорошо. Решение этой проблемы – рисование на спря‐ танном окне, и передача его в видимое окно только после того, как оно будет готово к отображению. Предикат W= pictOpen(Rectangle)
открывает скрытое окно для того, чтобы вы рисовали, не беспокоясь о мигающей анимации. Предикат Pict= pictClose(W)
создаёт изображение из содержимого скрытого окна. Кроме того, он закрывает окно. Наконец, предикат pictDraw(Win, Pict, pnt(10, 10), rop_SrcCopy)
передаёт изображение в видимое окно. Вот пример кода, содержащего эти три важных предика‐ та: snapshot(Win, S) :S:mov(), !, Rectangle= rct(0, 0, 200, 200), W= pictOpen(Rectangle), draw(W, S), Pict= pictClose(W), Win:pictDraw(Pict, pnt(10, 10), rop_SrcCopy).
Аргумент rop_SrcCopy определяет режим растровых операций. Он говорит, что система должна скопировать рисунок из исходного окна в назначенное. Есть другие режимы передачи изображений из одного окна в другое: • rop_SrcAnd, производит логическое И между битами рисунка и фоном. Вы можете ис‐ пользовать это для создания спрайтов. • rop_PatInvert, инвертирует цвета. • rop_SrcInvert инвертирует цвета источника.
98
%File draw.cl class draw open core, vpiDomains predicates classInfo : core::classInfo. snapshot:(windowGDI Win, objstate). end class draw %File draw.pro implement draw open core, vpiDomains, vpi class predicates draw:(windowHandle, objstate) procedure. clauses classInfo("playground/draw", "1.0"). draw(W, S) : S:segm(Rectangle), vpi::drawEllipse(W, Rectangle), fail. draw(_W, _S). snapshot(Win, S) : S:mov(), !, Rectangle= rct(0, 0, 200, 200), W= pictOpen(Rectangle), draw(W, S), Pict= pictClose(W), Win:pictDraw(Pict, pnt(10, 10), rop_SrcCopy). snapshot(_Win, _S). end implement draw
Рисунок 15.2 draw.cl and draw.pro
99
15.1.
Объектные факты
Обратите внимание на одну специфическую деталь: когда имеем дело с классами, состояние объекта устанавливается фактами класса (class facts); вот как факт класса объявляется: class facts city:(string Name, pnt Position). conn:(pnt, pnt). clauses city("Salt Lake", pnt(30, 40)). city("Logan", pnt(100, 120)). city("Provo", pnt(100, 160)). conn(pnt(30, 40) , pnt(100, 120)). conn(pnt(100, 120), pnt(100, 160)).
Факт класса является общим для всех объектов класса. Это означает, что если объектный преди‐ кат (предикат, объявленный в интерфейсе) изменяет факт, он изменяется для всех объектов класса. Поэтому, если сохранять состояние червя в факте класса, игра бы имела следующую неприятную особенность: Неприятная особенность игры – Когда игрок теряет червя, выходит из текущей игры, и начина‐ ет играть в следующую, он находит, что новый червяк не в изначальном положении. На самом деле, он в положении, где был потерян старый. Проблема в том, что факты класса, используемые для описания состояния червя имеют память класса (class memory), которая остаётся от одного червя к другому, от одного объекта червя к друго‐ му. Решение простое: объявите факты без добавления ключевого слова class к заголовку объявления; тогда для каждого объекта будет собственный набор фактов. Вот как объявить объектные факты: facts w:(integer, pnt) nondeterm. % worm d:(integer, integer) single. % direction clauses init() :- assert(w(1, pnt(110, 100))), assert(w(2, pnt(120, 100))), assert(w(3, pnt(130, 100))), assert(w(4, pnt(140, 100))), assert(w(5, pnt(140, 100))). d(10, 0).
Эта схема сохранения состояния объекта реализована в программе на рис. 15.3.
100
%File:objstate.cl class objstate : objstate end class objstate %File:objstate.i interface objstate open core, vpiDomains predicates init:(). turn:(integer, integer). mov:(). segm:(rct) nondeterm (o). end interface objstate %File:objstate.pro implement objstate open core, vpiDomains facts w:(integer, pnt) nondeterm. % червь d:(integer, integer) single. % направление clauses init() :- assert(w(1, pnt(110, 100))), assert(w(2, pnt(120, 100))), assert(w(3, pnt(130, 100))), assert(w(4, pnt(140, 100))), assert(w(5, pnt(140, 100))). d(10, 0). mov() :- retract(w(1, P)), P= pnt(X1, Y1), d(DX, DY), P1= pnt(X1+DX, Y1+DY), assert(w(1, P1)), retract(w(2, P2)), assert(w(2, P)), retract(w(3, P3)), assert(w(3, P2)), retract(w(4, P4)), assert(w(4, P3)), retract(w(5, _)), assert(w(5, P4)), !. mov(). segm(rct(X, Y, X+10, Y+10)) :- w(_, pnt(X, Y)). turn(DX, DY) :- assert(d(DX, DY)). end implement objstate Рисунок 15.3 objstate
101
Так как я начал эту главу разговаривая о Лопе де Вега (Lope de Vega), я не дам вам закончить без того, чтобы не попробовать его поэмы. Я не слишком хорошо знаю испанский, но так как я говорю на латыни, я могу понять значение любого текста на романском языке. Если вы не настолько удачливы, чтобы знать латынь, и не говорите по‐испански, попросите друга из соседнего университета помочь вам с переводом.
Daba sustento a un pajarillo un día Lucinda, y por los hierros del portillo fuésele de la jaula el pajarillo al libre viento en que vivir solía. Con un suspiro a la ocasión tardía tendió la mano, y no pudiendo asillo, dijo (y de las mejillas amarillo volvió el clavel que entre su nieve ardía): ¿Adónde vas por despreciar el nido, al peligro de ligas y de balas, y el dueño huyes que tu pico adora?. Oyóla el pajarillo enternecido, y a la antigua prisión volvió las alas, que tanto puede una mujer que llora. Lope de Vega
102
Глава 16: Анимация Пожалуйста, взгляните на главу 7, если вы не помните, как пользоваться событием painting. Вам также понадобится использовать маски для создания рисунков с прозрачным фоном; эта тема тоже была рассмотрена в главе 7. Вы также можете вспомнить использование GDIObject, которое сделано в разделе 3.2, где вы также найдёте материал об обновлении (invalidating’e) прямоугольника для ри‐ сования. • Создайте проект Project Name: rattlesnake UI Strategy: Object-oriented GUI (pfc/gui)
В этом проекте вы анимируете знаменитую гремучую змею, которая присутствует на флаге Джо‐ на Пол Джонса (John Paul Jones) «Не наступите на меня!» (Don’t thread on me). • Создайте новый пакет, и пусть его имя будет snake. • Создайте новую форму canvas. Вложите её в пакет snake. • Включите File/New. Затем, добавьте onFileNew(S, _MenuTag) :F= canvas::new(S), F:show(), click::bip(F).
к TaskWindow.win/CodeExpert/Menu/TaskMenu/id_file/id_file_new. •
Создайте класс click для хранения событий часов. Отключите Creates Objects. Используй‐ те код, данный на рис. 15.1 для определения и реализации click.
%File: click.cl class click open core predicates bip:(window). kill:(window). end class click %File: click.pro implement click open core class facts tm:integer := 0. clauses bip(W) :- tm := W:timerSet(1000). kill(W) :- W:timerKill(tm). end implement click
Рисунок 16.1 Класс click разбирается с таймером
16.1.
dopaint
Класс dopaint будет разбираться с рисованием, и работает более или менее похоже на метод dopaint в Java. Создайте класс, назовите его dopaint и отключите Creates Objects. Вот объявление класса: %File: dopaint.cl class dopaint
103
open core predicates classInfo : core::classInfo. draw:(windowGDI). invalidrectangle:(vpiDomains::rct) procedure (o). end class dopaint
Наконец, рис. 15.2 показывает реализацию dopaint.pro.
16.2.
Управление таймером
Когда вы закрываете canvas, вы должны отключить таймер. Чтобы достичь этой цели, добавьте onDestroy(W) :- click::kill(W).
к ProjectTree/canvas.frm/CodeExpert/Window/Destroy. Затем, добавьте onTimer(_Source, _TimerID) :dopaint::invalidRectangle(R), invalidate(R).
к ProjectTree/canvas.frm/CodeExpert/Miscellaneous/timer. Наконец, добавьте обработчик события onPaint(_Source, _Rectangle, GDIObject) :dopaint::draw(GDIObject).
к ProjectTree/canvas.frm/CodeExpert/Window/Paint.
16.3.
Как программа работает
Первое, что делает предикат draw(W) это рисует несколько лягушек на заднем плане. P= vpi::pictLoad("figs\\frogs.bmp"),
Конечно же, вы должны иметь файл frogs.bmp в папке figs. Затем, draw(W) получает изображение и маску змеи. flipflop(Snake, Mask), !,
Предикат flipflop разработан для смены двух изображений змеи, для получения иллюзии движения. Чтобы вставить змею на задний план, вы должны использовать маски. Если вы не помни‐ те, как это делать, прочтите главу 7 заново.
104
implement dopaint open core, vpiDomains constants className = "snake/snakestate". classVersion = "". class facts yesno:integer := 0. class predicates flipflop:(picture Picture, picture Mask) determ (o, o). clauses classInfo(className, classVersion). flipflop(Pict, Mask) :- yesno= 0, yesno := 1, Pict= vpi::pictLoad("figs\\n0.bmp"), Mask= vpi::pictLoad("figs\\n0Mask.bmp"), !. flipflop(Pict, Mask) :- yesno= 1, yesno := 0, Pict= vpi::pictLoad("figs\\n1.bmp"), Mask= vpi::pictLoad("figs\\n1Mask.bmp"). draw(W) :P= vpi::pictLoad("figs\\frogs.bmp"), W:pictDraw(P, pnt(10, 10), rop_SrcCopy), flipflop(Snake, Mask), !, W:pictDraw(Mask, pnt(40, 50), rop_SrcAnd), W:pictDraw(Snake, pnt(40, 50), rop_SrcInvert). draw(_). invalidRectangle(rct(40, 50, 100, 100)). end implement dopaint
Рисунок 16.2 dopaint.pro
105
Глава 17: Текстовый редактор В этой главе вы научитесь использовать класс editControl для создания текстового редактора. • Создайте новый проект Project Name: editor UI Strategy: Object-oriented GUI(pfc/gui)
• •
Добавьте пакет: forms Добавьте пакет editControl, который находится в каталоге установки Visual Prolog. Вы‐ берите пункт File/Add и ищите пакет в
Visual Prolog 6.x\pfc\gui\controls\editorControl.
•
Создайте новую форму edform.frm внутри пакета forms, как показано на рис. 17.3. Чтобы вставить editorControl в форму, выберите custom control – пункт нестандартного элемента управления на прототипе формы; это кнопка с ключом Йале1 (Yale) (рис. 17.1); и затем выберите editorControl в диалоговом окне, показанном на рис. 17.2
Рисунок 17.1 Кнопка вставки нестандартного элемента управления
Рисунок 17.2 Диалоговое окно Choose Custom Control
1
Йале изобрёл этот вид ключа, который так сегодня популярен. Легенда гласит, что вскоре после его изобретения, Гудини смог пробиться через новое устройство. – прим. авт.
106
Рисунок 17.3 Форма с Edit Control
•
Включите File/New и добавьте нижеприведённый фрагмент к нему.
onFileNew(S, _MenuTag) :- F= edform::new(S), F:show().
Смените название элемента управления на editorControl_ctl как показано на рис. 17.4.
17.1.
Сохранение и загрузка файлов
Идите в дерево проекта, щелкните правой кнопкой мыши по editor.frm, войдите в эксперт кода и добавьте фрагмент с рис. 17.5 к Control/save_ctl→onSave. Возвращайтесь в дерево проекта и снова щелкните правой кнопкой мыши по editor.frm. Выберите пункт Code Expert. В Dialog and Window Expert для edform добавьте код, показанный на рис. 17.6 к Control/load_ctl→onLoad. Это поч‐ ти всё, что вам нужно сделать для создания функционирующего текстового редактора.
Рисунок 17.4 Свойства элемента управления
107
predicates onSave : button::clickResponder. clauses onSave(_Source) = button::defaultAction() :Txt= editorControl_ctl:getEntireText(), FName = vpiCommonDialogs::getSaveFileName("*.*", ["Texto", "*.txt"], ""), file::existFile(FName), !, file::writeString(FName, Txt). onSave(_Source) = button::defaultAction().
Рисунок 17.5 Код для onSave
predicates onLoad : button::clickResponder. clauses onLoad(_Source) = button::defaultAction() :FName = vpiCommonDialogs::getOpenFileName("*.*", ["Texto", "*.txt"], ""), file::existFile(FName), !, Str= file::readString(FName, _IsUnicodeFile), editorControl_ctl:pasteStr(1, Str). onLoad(_Source) = button::defaultAction().
Рисунок 17.6 Код для onLoad.
108
Глава 18: Печать В предыдущей главе вы изучили, как построить текстовый редактор. В этой главе вы получите общее представление о печати. • Создайте проект Project Name: testPrt UI Strategy: Object-oriented GUI (pfc/GUI) Target Type: Exe Base Directory: C:\vip\codeForTyros
• • •
Откомпилируйте приложение, для того, чтобы внести прототипы Task Window в дерево проекта. В дереве проекта, дважды щёлкните по ветке TaskMenu.mnu, и включите пункт &File/&New/tF7 из прототипа меню. В дереве проекта, щёлкните правой кнопкой мыши по TaskWindow.win, и выберите пункт Code Expert из контекстного меню, как на рис. 2.7. Появится Dialog and Window Expert. От‐ кройте папки Menu, TaskMenu и id_file, как показано ниже.
Выберите ветку id_file_new и нажмите кнопку Add. Затем, дважды щёлкните по свежесозданной ветке id_file_new→on_file_new; наконец, замените прототип onFileNew(_Source, _MenuTag) фраг‐ ментом, данным ниже. clauses onFileNew(_Source, _MenuTag) :PW=vpi::printStartJob("Recoreco"), _HRES = vpi::winGetAttrVal(PW,attr_printer_hres), VRES = vpi::winGetAttrVal(PW,attr_printer_vres), V_SCR_RES=vpi::winGetAttrVal(PW,attr_screen_vres), FNT=vpi::fontCreate(ff_Fixed,[], VRES*40 div V_SCR_RES), vpi::winSetFont(PW,FNT), vpi::printStartPage(PW), vpi::drawText(PW, 100, 200, "Before the sunset!"), vpi::printEndPage(PW), vpi::printEndJob(PW).
Красота схемы печати Visual Prolog в том, что вы имеете дело с принтером, как если бы это было обычное графическое окно. Фактически, первое, что вы должны сделать, это открыть окно работы принтера. PW=vpi::printStartJob("Recoreco")
Как только у вас будет окно, вы можете использовать его для получения информации о разреше‐ нии принтера. _HRES= vpi::winGetAttrVal(PW,attr_printer_hres), VRES= vpi::winGetAttrVal(PW,attr_printer_vres), V_SCR_RES=vpi::winGetAttrVal(PW,attr_screen_vres),
Методом проб и ошибок, а также используя информацию о разрешении принтера, вы можете определить шрифт, который хорошо выглядит при печати.
109
FNT=vpi::fontCreate(ff_Fixed,[], VRES*40 div V_SCR_RES), vpi::winSetFont(PW,FNT),
Наконец, вы можете использовать любой предикат рисования для производства неплохо отпеча‐ танной страницы. vpi::drawText(PW, 100, 200, "Before the sunset!"),
110
Глава 19: Баги Старые компьютеры были основаны на реле, которые представляют собой громоздкие электри‐ ческие устройства, обычно содержащие в себе электромагнит, который активируется напряжением в одной цепи для включения или выключения другой цепи. Компьютеры, сделанные из подобных ве‐ щей, были огромными, медленными и ненадёжными. Поэтому, 9го сентября 1945 года моль влетела в одно из реле компьютера Harvard Mark II и угробила его. C того времени bug – баг стало стандарт‐ ным словом для обозначения ошибки, которая препятствует компьютеру работать как предназначе‐ но. Из‐за багов, компилятор Visual Prolog часто возвращает сообщения об ошибках, вместо того, что‐ бы создавать код и запускать соответствующие программы. На самом деле, компилятор Visual Prolog выдаёт больше сообщений об ошибках, чем любой другой язык, кроме, возможно, Clean. Студенты не ценят это постоянное ворчание об ошибках, и редко пытаются проанализировать сообщения об ошибках, но поверьте мне, вы должны поблагодарить разработчиков такого компилятора, который даёт вам шанс вычистить все пятна, которые могут повредить вашей программе. Как я уже сказал, Visual Prolog хорош в локализации багов. Несмотря на это, если вы не научитесь разбираться с сооб‐ щениями об ошибках, вы найдёте дебаггинг случайным хождением, которое почти никогда не ведёт к успеху. Поэтому, в этой главе, вы изучите, как интерпретировать сообщения от компилятора.
19.1.
Ошибки типа
В программе ниже нет багов; однако, если вам нужно использовать беззнаковое целое, которое получается от функции факториала для, скажем, вычисления двоичного числа, компилятор может отказаться выполнять это. implement bugs open core clauses classInfo("bugs", "1.0"). class predicates fact:(unsigned) -> unsigned. clauses fact(N) = F :- if N < 1 then F=1 else F=N*fact(N-1) end if. run():- console::init(), stdio::write("N: "), N= stdio::read(), stdio::write(fact(N)), stdio::nl. end implement bugs goal mainExe::run(bugs::run).
Например, если вы попробуете откомпилировать программу с рис. 19.1, вы получите следующее сообщение об ошибке: error c504: The expression has type '::integer', which is incompatible with the type '::unsigned'
Ошибка исчезнет, если вы явно преобразуете каждый входной аргумент binum/3 в беззнаковое целое; вы также должны преобразовать выходной аргумент факториала в целое. binum(N, P, R) :N1= convert(unsigned, N), P1= convert(unsigned, P), R = convert(integer, fact(N1) div (fact(P1)*fact(N1-P1))).
111
implement bugs open core clauses classInfo("bugs", "1.0"). class predicates fact:(unsigned) -> unsigned. binum:(integer, integer, integer) procedure (i, i, o). clauses fact(N) = F :if N < 1 then F=1 else F=N*fact(N-1) end if. binum(N, P, R) :- R = fact(N) div (fact(P)*fact(N-P)). run():- console::init(), stdio::write("N: "), N= stdio::read(), stdio::write("P: "), P= stdio::read(), binum(N, P, R), stdio::write(R), stdio::nl. end implement bugs goal mainExe::run(bugs::run).
Рисунок 19.1. Забагованная программа.
19.2.
Не‐процедура внутри процедуры
Это иной баг, который доставляет много сложностей начинающим. Visual Prolog объявляет неко‐ торые предикаты как процедуры. Это случай предиката run(), который имеется во всех консольных программах. Это означает, что вы не можете вызвать nondeterm предикат из run(). Таким образом, если вы попытаетесь откомпилировать программу, приведённую на рис. 19.2, вы получите следую‐ щее сообщение об ошибке: error c631: The predicate 'nonprocedure::run/0', which is declared as 'procedure', is actually 'nondeterm'
Это правда, что, получив целочисленный аргумент, weight/2 должен действовать как процеду‐ ра. Однако Пролог глух к принятию желаемого за действительное; тогда вам необходимо определить процедуру getweight/2, которая будет извлекать значения веса. Ниже вы можете увидеть, как getweight может быть определено. Используйте это как модель для выхода из ситуаций, где вам надо вызвать nondeterm предикат из процедуры. implement nonprocedure class facts weight:(integer, real). class predicates getweight:(integer, real) procedure (i, o). clauses classInfo("nonprocedure", "1.0"). weight(0, -1.0). weight(1, 2.0). weight(2, 3.5). getweight(I, R) :- weight(I, R), ! or R=0. run():- console::init(), getweight(1, X), stdio::write(X), stdio::nl. end implement nonprocedure goal mainExe::run(nonprocedure::run).
112
19.3.
Неопределённые условия.
В Прологе есть конструкции, которые не принимают nondeterm предикаты. Например, условием конструкции if-then-else обязан быть determ предикат. Поэтому, программа на рис. 19.3 выведет сообщение об ошибке: error c634: The condition part of if-then-else may not have a backtrack point (almost determ)
Ошибка исчезнет, если вы определите member как determ предикат: class predicates member:(string, string*) determ. clauses member(X, [X|_]) :- !. member(X, [_|Xs]) :- member(X, Xs).
implement nonprocedure class facts weight:(integer, real). clauses classInfo("nonprocedure", "1.0"). weight(0, -1.0). weight(1, 2.0). weight(2, 3.5). run():- console::init(), weight(1, X), stdio::write(X), stdio::nl. end implement nonprocedure
Рисунок 19.2. Баг, вызванный недетерминистическим предикатом.
implement ifexemple class predicates vowel:(string, string) procedure (i, o). member:(string, string*) nondeterm. clauses classInfo("ifexample", "1.0"). member(X, [X|_]). member(X, [_|Xs]) :- member(X, Xs). vowel(X, Ans) :- if member(X, ["a", "e", "i", "o", "u"]) then Ans= "yes" else Ans="no" end if. run():- console::init(), vowel("e", Ans), stdio::write(Ans). end implement ifexemple Рисунок 19.3. Требуется детерминистический предикат.
113
19.4.
Невозможно определить тип
Есть случаи, когда необходимо определить тип до конкретизации переменной; спецификация типа может быть произведена процедурой hasdomain(type, Var)
В бесполезной программе приведённой ниже, если убрать hasdomain(integer, X), то полу‐ чим сообщение об ошибке, утверждающее, что невозможно определить тип терма X. implement vartest clauses classInfo("vartest", "1.0"). run() :- console::init(), hasdomain(integer, X), X= stdio::read(), stdio::write(X, X, X), stdio::nl. end implement vartest goal mainExe::run(vartest::run).
19.5.
Шаблон потока
Когда вы не объявляете шаблон потока, Пролог предполагает, что все аргументы являются вход‐ ными. Так как это упрощённое предположение очень часто бывает неверным, вы получите сообще‐ ние об ошибке, как в примере ниже. implement dataflow class predicates factorial:(integer, integer). clauses classInfo("dataflow", "1.0"). factorial(0, 1) :- !. factorial(N, N*F1) :- factorial(N-1, F1). run():- console::init(), N= toTerm(stdio::readLine()), factorial(N, F), stdio::write(F). end implement dataflow goal mainExe::run(dataflow::run).
Вы можете исправить баг, объявив factorial как входную/выходную процедуру. class predicates factorial:(integer, integer) procedure (i, o).
114
Глава 20: Менеджер баз данных Эта глава показывает, как создать форму с edit fields – полями ввода и использовать их содержи‐ мое для заполнения базы данных. Однако, самое важное в том, что вы научитесь получать доступ к полям из формы. Окно сохраняется в факте‐переменной, чьё название вы должны узнать, и исполь‐ зовать для управления окном, как в примере ниже. onSave(_Source) = button::defaultAction() :- Key= keyvalue_ctl:getText(), Email= email_ctl:getText(), T= listEdit_ctl:getText(), emails::insert_info(Key, Email, T).
20.1.
Менеджер базы данных
. Базы данных очень полезны в любом коммерческом приложении. Если вы не много знаете о базах данных, я сильно рекомендую, чтобы вы поискали в интернете или в вашей местной библиоте‐ ке книгу по этой теме. Основные понятия B+Tree базы данных: Indexes – Индексы, которые хранятся в специальной структуре данных под названием B+Tree. Всё, что вы должны знать про B+Tree это то, что они хранят вещи в порядке (например, в алфавитном порядке). Когда вещи упорядочены, их легче искать. Например, легко искать слова в словаре, потому что его элементы расположены в алфавитном порядке. Domains – Домены определяют структуру записей. В нашем примере, домен клиента определён как domains contact=email(string, string, string).
References – Ссылки являются указателями на места, в которых хранятся записи. После этого краткого введения, давайте опишем программу. Не забывайте, что наша цель – не научиться строить менеджер базы данных, а показать, как работать с формами. • Создайте проект, чьё название emailBook, с объектно‐ориентированным GUI • Используя пункт File/Add из панели задач, добавьте пакет chainDB. Этот пакет находится внутри папки pfc в каталоге установки. Откомпилируйте приложение. • Создайте класс emails в корне дерева проекта. Уберите Creates Objects. В разделе 20.2 вы найдёте листинги emails.cl и emails.pro, которые вы должны вставить в соответ‐ ствующие файлы. Откомпилируйте приложение. • Добавьте новый элемент в панель задач приложения. Для того, чтобы это сделать, идите в окно дерева проекта и дважды щёлкните по TaskMenu.mnu. В диалоговом окне Task Menu щёлкните правой кнопкой мыши и выберите New из появившегося меню. В соответ‐ ствующем поле напишите Create, что будет названием элемента меню. Откомпилируйте приложение. • Добавьте следующий код class predicates createNewDB:(string FNAME). predicates onCreate : window::menuItemListener. clauses onCreate(_Source, _MenuTag) : FName = vpiCommonDialogs::getSaveFileName("*.*", ["Email", "*.dbs"], ""), !, createNewDB(FName). onCreate(_, _). createNewDB(Fname) :- file::existfile(Fname), !. createNewDB(Fname) :- emails::data_base_create(Fname).
115
к Project Window\TaskMenu.win\Code Expert\Menu\id_create. Вы должны разрабатывать ваши программы шаг за шагом, проверяя, верен ли каждый новый шаг, перед тем, как двигаться дальше. В текущем шаге, вы добавили новый элемент меню Create в TaskMenu.mnu. Вы также вставили код в onCreate, для чтения имени файла и создания соответст‐ вующей базы данных. Откомпилируйте приложение, запустите его и выберите пункт Create меню приложения. Когда диалоговое окно выбора файла спросит вас об имени файла, наберите mails.db. Если всё будет идти по сценарию, программа создаст файл mails.db. Когда вам нужно использовать базу данных, вам нужно её открыть. После использования её, вы должны закрыть её, до того, как выключить компьютер. Чтобы удостовериться, что мы не забыли за‐ крыть базу данных до выхода из приложения, давайте добавим фрагмент clauses onDestroy(_) :- emails::data_base_close().
к Project Tree/TaskWindow.win/Code Expert/Window/onDestroy. Теперь, когда вы уверены, что никто не забудет закрыть базу данных, вы можете продолжать с её открытием. Из Project Window, дважды щёлкните по TaskMenu.mnu, и включите элемент меню &Open\tF8. Добавьте фрагмент class predicates openDB:(string FNAME). predicates onFileOpen : window::menuItemListener. clauses onFileOpen(_Source, _MenuTag) : FName = vpiCommonDialogs::getOpenFileName("*.*", ["Email", "*.dbs"], ""), !, openDB(FName). onFileOpen(_, _). openDB(Fname) :- file::existfile(Fname), !, emails::data_base_open(Fname). openDB(_).
к ProjWin/TaskWindow.win/Code Expert/Menu/TaskMenu/id_file/id_file_open. Откомпилируйте приложение. Затем, из панели задач VDE, выберите пункт File/New, и вставьте нижеприведённую форму в корень проекта.
На этой форме есть list box – раскрывающийся список. Следовательно, необходимо добавить в него список вариантов. Чтобы достичь этой цели, добавьте фрагмент clauses onShow(_Source, _CreationData) : WlboxHandle=listEdit_ctl:tryGetVpiWindow(), !, vpi::lboxAdd(WlboxHandle, ["Person", "Commerce"]). onShow(_, _).
116
к ProjWin/insertForm.frm/Code Expert/Window/Show. Последний шаг этого проекта – добавить код к кнопкам Save и List. Поэтому, добавьте onSave(_Source) = button::defaultAction() : Key= key_ctl:getText(), Email= address_ctl:getText(), T= listEdit_ctl:getText(), emails::insert_info(Key, Email, T).
к ProjWin/insertForm.frm/Code Expert/Control/save_ctl. Наконец, добавьте clauses onList(_Source) = button::defaultAction() : emails::db_list().
к ProjWin/insertForm.frm/Code Expert/Control/list_ctl. Откомпилируйте приложение ещё раз, и на этом всё.
20.2.
Класс emails
%File: emails.cl class emails open core predicates classInfo : core::classInfo. data_base_create:(string FileName) procedure (i). data_base_open:(string FileName). data_base_close:(). insert_info:(string Name, string Contact, string Tipo). data_base_search:(string Name, string Contact, string Tipo) procedure (i, o, o). db_list:(). del:(string). end class emails %File: emails.pro implement emails open core domains contact= email(string, string, string). class facts - dbState dbase:(chainDB, chainDB::bt_selector) determ. clauses classInfo("db/emails/emails", "1.0"). data_base_create(FileName) :ChainDB= chainDB::db_create(FileName, chainDB::in_file), ChainDB:bt_create("email", ContactName, 40, 21, 1), ChainDB:bt_close(ContactName), ChainDB:db_close(), !. data_base_create(_). data_base_open(FileName) :ChainDB= chainDB::db_open(FileName, chainDB::in_file), ChainDB:bt_open("email", ContactName), assert(dbase(ChainDB, ContactName)). data_base_close() :retract(dbase(ChainDB, BTREE)), !, ChainDB:bt_close(BTREE), ChainDB:db_close(). data_base_close().
117
insert_info(Key, Email, Tipo) :dbase(ChainDB, BTREE), !, ChainDB:chain_insertz( "Contacts", email(Key, Email, Tipo), RefNumber), ChainDB:key_insert(BTREE, Key, RefNumber). insert_info( _, _, _). data_base_search(Name, Contact, Tipo) :dbase(ChainDB, BTREE), ChainDB:key_search(BTREE, Name, Ref), ChainDB:ref_term(Ref, Term), Term= email(Name, Contact, Tipo), !. data_base_search(_Name, "none", "none"). class predicates getInfo:(chainDB, chainDB::ref, string, string) procedure (i, i, o, o). dbLoop:(). clauses getInfo(ChainDB, Ref, Name, Email) :ChainDB:ref_term(Ref, Term), Term= email(Name, Email, _), !. getInfo(_, _, "none", "none"). db_list() :dbase(ChainDB, BTREE), ChainDB:key_first(BTREE, Ref), !, getInfo(ChainDB, Ref, Name, Email), stdio::write(Name, ", ", Email), stdio::nl, dbLoop(). db_list() :- stdio::write("None"), stdio::nl. dbLoop() :- dbase(ChainDB, BTREE), ChainDB:key_next(BTREE, Ref), !, getInfo(ChainDB, Ref, Name, Email), stdio::write(Name, ", ", Email), stdio::nl, dbLoop(). dbLoop(). del(Key) :dbase(ChainDB, BTREE), ChainDB:key_search(BTREE, Key, Ref), !, ChainDB:key_delete(BTREE, Key, Ref), ChainDB:term_delete("Contacts", Ref). del(_Key). end implement emails
20.3.
Менеджер баз данных
Функция, которая создаёт базу данных, очень проста: data_base_create(FileName) : ChainDB= chainDB::db_create(FileName, chainDB::in_file), ChainDB:bt_create("email", ContactName, 40, 21, 1), ChainDB:bt_close(ContactName), ChainDB:db_close(), !. data_base_create(_).
Метод db_create/2 делает то, что означает, т.е., он создаёт базу данных внутри FileName и возвращает объект, чтобы ей управлять. Объект сохранён в ChainDB. Из ChainDB вызывается
118
bt_create для получения BTree; пакет BTree инициализирует переменную ContactName указате‐ лем на BTree. Наконец, закрывается и BTree, и база данных.
Открывается база данных с помощью предиката, который почти такой же простой, как и тот, что использовался для её создания. data_base_open(FileName) : ChainDB= chainDB::db_open(FileName, chainDB::in_file), ChainDB:bt_open("email", ContactName), assert(dbase(ChainDB, ContactName)). data_base_close() : retract(dbase(ChainDB, BTREE)), !, ChainDB:bt_close(BTREE), ChainDB:db_close(). data_base_close().
Предикат db_open открывает базу данных, сохранённую в FileName, и возвращает объект, ко‐ торый может быть использован для управления ею; объект сохраняется в ChainDB, который исполь‐ зуется для открытия email-BTree. И ChainDB, и указатель BTree сохраняются в факт‐предикате, для будущих ссылок. assert(dbase(ChainDB, ContactName))
Когда пользователь закрывает приложение, предикат data_base_close отменяет менеджер баз данных и указатель BTree из базы данных фактов, и закрывает и BTree, и ChainDB. До этой точ‐ ки, мы описывали предикаты, которые администрируют базу данных; теперь опишем метод, кото‐ рый можно использовать для внесения данных в базу данных. insert_info(Key, Email, Tipo) :- dbase(ChainDB, BTREE), !, ChainDB:chain_insertz( "Contacts", email(Key, Email, Tipo), RefNumber), ChainDB:key_insert(BTREE, Key, RefNumber). insert_info( _, _, _).
База данных это наполняющаяся система. Как любая наполняющаяся система, она имеет два компонента: цепь папок, каждая содержит данные, которые необходимо хранить; и алфавитный спи‐ сок, который указывает на различные папки и позволяет потенциальному пользователю узнать, где хранится желаемая информация. В примере базы данных, цепь папок называется Contacts; каждая папка имеет следующую форму: email(string, string, string). Первый аргумент email хра‐ нит название контакта, второй содержит электронное письмо, и третий информирует о типе контакта, имеющимся с предметом, ассоциированным с вхождением базы данных. Предикат chain_insertz сохраняет папку в цепь Contacts и возвращает RefNumber на его ме‐ стоположение. RefNumber будет сохранён в алфавитном порядке в BTree. Чтобы найти электронное письмо Contact, соответствующее заданному Name, ищут в BTree, для того, чтобы найти ссылку; обладая номером ссылки, получить папку просто. data_base_search(Name, Contact, Tipo) :- dbase(ChainDB, BTREE), ChainDB:key_search(BTREE, Name, Ref), ChainDB:ref_term(Ref, Term), Term= email(Name, Contact, Tipo), !. data_base_search(_Name, "none", "none").
Предикат, использованный для удаления элемента, легко понять и я не буду на нём останавли‐ ваться. Предикат, который перечисляет содержимое базы данных в алфавитном порядке много бо‐ лее комплексный, но, когда вы знаете базовые принципы управления базами данных, не сложный для понимания.
119
Глава 21: Книги и статьи В этой главе я поговорю о некоторых книгах по Прологу и техникам программирования. Вы заме‐ тите, что книги несколько стары, и больше не печатаются. Одна причина этого в том, что люди не по‐ купают техническую литературу так часто, как раньше, так как большинство студентов надеются по‐ лучить материал вроде этого руководства из интернета. Другая причина в том, что многие великие компьютерные учёные были увлечены Прологом в восьмидесятые и девяностые. Эти учёные написа‐ ли книги, которые сложно переплюнуть. Например, я не могу найти современной книги, которая бы приблизилась к [Coelho/Cotta].
21.1.
Грамматика
Пролог превосходен в обработке языков, написании компиляторов и подготовке документов. Фактически, язык был изобретён с расчётом на обработку естественных языков. Большинство книг работают со стандартным Прологом, который немного отличается от Visual Prolog. В то время как стандартный Пролог был разработан для быстрых прототипов маленьких приложений и для провер‐ ки идей, Visual Prolog используется в крупных коммерческих и промышленных критических системах. Веллесли Баррос (Wellesley Barros), например, разработал и запрограммировал крупную систему для администрирования госпиталя в Visual Prolog. Будьте уверены, что в подобной системе нельзя позво‐ лить себе аварии из‐за ошибок типа. В критических ситуациях, проверки и объявления типов очень важны. В любом случае, несмотря на то, что вы программируете в Visual Prolog, вы можете извлечь пользу из идей, представленных в книгах, основанных на стандартном Прологе. Старая книга, кото‐ рую вы можете найти в вашей местной библиотеке, была написана аргентинским ученым Вероникой Дал (Veronica Dahl) (см. [Abramson/Dahl]), одной из величайших ныне живущих лингвистов. Доктор Седрик де Карвало (Cedric de Carvalho) написал компилятор Пролога, который генерирует байт‐код Java. Этот компилятор позволит вам писать в Прологе апплеты. Его компилятор ещё не до конца встал на ноги, но вы легко можете его усовершенствовать. Между тем, вы многое изучите о компиляции, грамматическом разборе, Java, и т. д. Вы можете начать читать статьи Седрика в [Cedric et all]. Вы также можете портировать компилятор, написанный в Visual Prolog 5.2, в Visual Prolog 6.2. Адрес: http://netProlog.pdc.dk
Другая книга по обработке естественных языков со множеством интересных подходов к грамма‐ тическому формализму это [Pereira/Shieber]. Эта книга более дидактична, чем [Abramson/Dahl]. На самом деле, [Abramson/Dahl] написана для специалистов, а [Pereira/Shieber] говорит со студентами, которые ещё только учатся сложному искусству обработки языков. Книга, не специализирующаяся на обработке естественных языков, но дающая хорошее введение в неё, это [Sterling and Shapiro]. Если вы хотите иметь продвинутую книгу по этой теме, хорошим выбором является книга [Gazdar/Mellish].
21.2.
Базы данных
Дэвид Уоррен (David Warren), компьютерный учёный, является главным сторонником использо‐ вания Пролога как движка для баз данных. Эта книга [Warren] больше не печатается, но вы должны достать копию с рук, если вы хотите изучать базы данных. Она классическая. Дэвид Уоррен ведет команду, разрабатывающую XSB – Пролог, ориентированный на базы дан‐ ных, в Stony Brook University. Хотя я не думаю, что эта версия Пролога используема для серьёзных приложений, так как она полагается на иные инструменты для построения GUI, не компилируется (написать красивое приложение в XSB нелегко), и не типизирована, я очень рекомендую посетить страницу XSB. Там вы найдёте множество хороших идей о базах данных и Пролог, и инструментах их разработки. Адрес http://www.cs.sunysb.edu/~sbПролог/xsb-page.html
120
Между прочим, XSB предлагает набор инструментов для экспорта его функциональности в дру‐ гие языки. Таким образом, если вам нравятся его особенности, вы можете вызвать DLL XSB из Visual Prolog! Страница XSB показывает, как вызвать DLL из Visual Basic. Вы можете использовать тот же под‐ ход, чтобы вызвать её из Visual Prolog.
21.3.
Техники программирования
Если вам было сложно разбираться с такими понятиями, как рекурсивное программирование, операции со списками, отсечения, недетерминированное программирование и parsing – граммати‐ ческий разбор – вам надо прочитать книгу The Art of Пролог (см. [Sterling and Shapiro]). Идеи, пред‐ ставленные в этой книге, легко могут быть перенесены в Visual Prolog. В Visual Prolog вы даже можете улучшить оригинальные программы. Например, один из моих студентов смог создать диаграммы для электрических цепей, описанных в главе 2 [Sterling and Shapiro]. Моя самая любимая книга по Пролог это Prolog by Example, [Coelho/Cotta]. Это вроде FAQ: авторы ставят проблему и показывают решение. Это также антология. Проблемы и решения были предло‐ жены и решены великими компьютерными учёными. Эта книга заслуживает нового издания. Пытай‐ тесь, и очень сильно, достать копию с рук. Если вы не можете получить копию, попросите разреше‐ ния авторов и сделайте ксерокопию.
121
Библиография [Abramson/Dahl]
Harvey Abramson and Veronica Dahl, "Logic Grammars", Springer Verlag, 1989.
[Cedric et all]
de Carvalho, C. L., Costa Pereira, A. E. and da Silva Julia, R. M. "Data‐flow Synthesis for Logic Programs". In: System Analysis Modeling Simulation, Vol 36, pp. 349‐366, 1999.
[Warren]
David S. Warren. "Computing With Logic: Logic Program‐ ming With Prolog". Addison‐Wesley / January 1988.
[Sterling and Shapiro]
Sterling and Shapiro. "The Art of Prolog". MIT Press / Janu‐ ary 1986. [Стерлинг Л., Шапиро Э. «Искусство программирования на языке Пролог». Пер. с англ. Сопрунова С. Ф., Шабано‐ ва Ю. Г. Под ред. Дадаев Ю. Г. М.: Мир, 1990. – прим. пер.]
[Coelho/Cotta]
Coelho, H. and Cotta, J. "Prolog by Example". (1988) Berlin: Springer‐Verlag.
[HowToSolveItWithПролог]
Helder Coelho, Carlos Cotta, Luis Moniz Pereira. "How To Solve It With Prolog". Ministerio do Equipamento Social. Laboratorio Nacional de Engenharia Civil.
[Pereira/Shieber]
F.C.N. Pereira and S.M. Shieber. "Prolog and Natural‐ Language Analysis". CSLI Publications, 1987.
[Gazdar/Mellish]
Gazdar, G., and C.S. Mellish. "Natural Language Processing in Prolog". Addison‐Wesley.
122