Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции флп.doc
Скачиваний:
270
Добавлен:
09.02.2015
Размер:
3.68 Mб
Скачать

Доступность метапеременных

Характерной особенностью Пролога является эквивалентность программ и данных – и то и другое может быть представлено логическими термами. Для того чтобы можно было использовать эту эквивалентность, необходимо, чтобы программы можно было рассматривать в качестве данных, а данные можно было бы превращать в программы. В данном разделе описывается средство, позволяющее превращать терм в цель. Смысл предиката Call(X) в Прологе состоит в передаче терма Х в качестве цели для решения.

На практике в большинстве реализации языка Пролог отсутствует введенное нами ограничение для логических программ, а именно: цели в теле предложения должны быть термами, отличными от переменных. Доступность метапеременных означает, что в качестве целей в конъюнктивных вопросах и в теле предложений разрешается использовать переменные. В процессе вычисления в момент обращения к такой переменной ей должно быть сопоставлено значение-терм. Если переменной не сопоставлено значение в момент обращения, то возникает ошибочная ситуация. Доступность метапеременных является синтаксическим средством, обеспечивающим условия применения предиката call.

Доступность метапеременных является важным инструментом метапрограммирования, используемым, в частности, при построении метаинтерпретаторов и оболочек. Два важных примера подобных программ – простая оболочка (программа 12.6) и метаинтерпретатор (программа 19.1) будут обсуждаться в последующих главах. Это же средство существенно используется при определении отрицания (программа 11.5) и при определении предикатов высших порядков, описанных в разд. 17.3.

В заключение приведем пример использования метапеременных в определении логической дизъюнкции, обозначенной инфиксным бинарным оператором “;”. Цель (X; Y) выполнена, если Х и Y выполнено. Определение оформлено как программа 10.8.

X;Y

Х или Y

X; Y X.

X; Y  Y.

Программа10.8. Логическая дизъюнкция.

Лекция №8 Внелогические предикаты

В Прологе имеется ряд предикатов, существующих за рамками модели логического программирования. Они называются внелогическими предикатами. Такие предикаты в процессе решения логических целей порождают побочный эффект. Существуют три основных вида внелогических предикатов: предикаты, относящиеся к вводу-выводу, предикаты, обеспечивающие доступ к программе и ее обработку, и предикаты для связи с внешней операционной системой. В данной главе рассматриваются предикаты Пролога, связанные с вводом-выводом, и предикаты, обеспечивающие обработку программы. Интерфейс с операционной системой слишком системозависим.

Ввод-вывод

Очень важный класс предикатов, вызывающих побочные эффекты, образуют предикаты, относящиеся к вводу-выводу. Любой практический язык программирования должен обеспечивать средства ввода и вывода. Однако вычислительная модель Пролога препятствует введению операций ввода-вывода в виде чистых компонентов языка.

Основной предикат вводаread (X). При решении такой цели считывается терм из текущего входного потока; обычно это данные, вводимые с терминала. Введенный терм унифицируется с X, цель read выполнена или не выполнена в зависимости от результата унификации.

Основной предикат вывода – write (X). Решение этой цели приводит к записи терма Х в текущий выходной поток, определяемый операционной системой; обычно это данные, выводимые на терминал. Ни read, ни write не дают альтернативных решений при возврате.

Обычно в предикате read в качестве аргумента используется переменная, которая приобретает значение первого терма в текущем входном потоке. Сопоставление переменной Х какого-либо значения, определенного вне программы, не входит в логическую модель, так как каждое новое обращение к процедуре read(X) будет выполняться с (возможно) различными значениями X.

Предикат read проводит синтаксический анализ следующего терма в потоке ввода. Если найдены синтаксические ошибки, то на терминале печатается сообщение об ошибке и предпринимается попытка прочесть следующий терм.

Внелогическая природа предикатов read и write различна. Если в программе заменить все обращения к предикату write на обращения к всегда выполняемому предикату true, то семантика программы не изменится. Для предиката read это не так.

Различные версии Пролога содержат различные системозависимые вспомогательные предикаты.

Полезным вспомогательным предикатом является предикат writeln(Xs), аналогичный соответствующей команде языка Паскаль. Решение цели writeln(Xs) приводит к печати списка термов Xs в виде выходной строки. Предикат описан в программе 12.1. В ней использован системный предикат пl, вызывающий переход к выводу с новой строки.

Writeln([X | Xs]) write(X), writeln(Xs)

Writeln([ ]) nl.

Программа 12.1. Вывод списка термов.

Строка литер заключается в апострофы. Например, решение цели writeln ([ 'Значение Х равно', X]) приводит к печати текста

Значение Х равно 3

если переменной Х сопоставлено число 3.

В Прологе можно выполнять и более общие операции со строками литер, обрабатывая строки как списки кодов литер. Если, например, используются коды ASCII, то список [80,114,111,108,111,103] соответствует строке «Prolog». Код ASCII для литеры Р равен 80 для литеры r – 114 и т.д. Строка литер, заключенная в кавычки, является другим способом обозначения списка, состоящего из кодов ASCII. В нашем примере список можно задать с помощью строки «Prolog». Такие строки просто являются более удобной синтаксической записью подобных списков. Обработка строк может производиться с помощью стандартных методов работы со списками.

Системный предикат name(X, Y) используется для преобразования названия константы в строку литер и обратно. Цель name(X,Y) выполнена, если Xатом, а Y – список кодов ASCII, соответствующих литерам атома X, например, цель name (log,[108, 111, 103])? выполнена.

Ввод-вывод может выполняться не только на уровне термов с помощью предикатов read и write, но и на более низком уровне – на уровне литер. Основной предикат вывода на уровне литер имеет вид put(N), он помещает литеру, соответствующую коду ASCII для N, в текущий выходной поток. Основной предикат ввода get (X)1), сопоставляющий аргументу Х код ASCII первой литеры во входном потоке.

1) Это несколько отличается от операторов Edinburgh-Пролога.

Программа 12.2 задает вспомогательный предикат read_wordlist (Words), читающий список слов Words. В построении программы использован предикат gel. Слова могут быть разделены произвольным числом пробелов (код ASCII для пробела – 32) и могут состоять из любого числа строчных и прописных букв и символов подчеркивания. Последовательность слов завершается точкой.

read_word_list(Ws)

get(C),

read_word_list(C, Ws).

read_word_list(C,[W |Ws]) 

word_char(C),

read_word(C,W,C1),

read_word_list(C1,Ws).

read_word_list(C,Ws) 

fill_char(C),

get(Cl),

read_word_list(C1,Ws). read_word_list(C,[ ]) 

end_of_words_char(C).

read_word(C,W,C1) 

word_chars (C, Cs, С1), name(W.Cs).

word_chars(C, [С | Cs],C0)

word_char(C),

!,

get(C1),

word chars (C1,Cs, C0). word_chars(C,[ ],C)

not word_char(C).

word_char(C)97C,C122. % строчные буквы

word char(C) 65 C, С90. %прописные буквы

word_char(95). %подчеркивание

fillchar(32). % пробел

end of words_char(46). % точка

Программа 12.2. Чтение списка слов.

Предикат read_word_list читает литеру С и обращается к процедуре read_word_list(C,Words). В этой процедуре выполняется одно из трех действий в зависимости от значения литеры С. Если С – литера, входящая в слово, т.е. прописная или строчная буква, или символ подчеркивания, то обрабатывается следующее слово и далее рекурсивно обрабатывается последовательность оставшихся слов. Второе действие состоит в игнорировании пробелов, т. е. считывается следующая литера, и работа программы рекурсивно продолжается. Если, наконец, встретилась литера, обозначающая конец последовательности слов, то программа завершает работу и возвращает список слов.

Существенно, что всегда программа прежде читает литеру, а затем уже проверяет, что следует делать. Если это полезная литера, например литера слова, то она должна быть включена в слово. Иначе литеры могут быть потеряны при возврате. Рассмотрим следующий цикл ввода и обработки:

process([ ])

get(C), end_of_words_char(C).

process ([W | Words])

get(C), word _char(C), get_word(C, W), process (Words).

Если первая литера слова не удовлетворяет предикату end_of_words_char, то первое предложение не выполнится, а второе предложение приведет к вводу следующей литеры.

Возвращаясь к программе 12.2, заметим, что предикат read_words(C,W,C1) читает слово W, начинающееся с текущей литеры С, и возвращает литеру, следующую за словом – С1. Список литер, образующих слово, находится с помощью процедуры word_chars/3 (аргументы те же, что и у процедуры read_words). Слово строится по списку литер с помощью системного предиката name. Процедура words__chars также обладает свойством опережающего просмотра одной литеры, так что литеры не теряются.

Такие предикаты, как fill_char/1 и word_char/1, упрощают представление данных в Прологе.

Доступ к программам и обработка программ

До сих пор предполагалось, что программы размещены в памяти компьютера, и вопрос о том, как они там представлены и каким образом были туда занесены, не рассматривался. Однако многие приложения Пролога зависят от того, как организован доступ к предложениям программы. Более того, если программа должна изменяться во время вычислений, то следует иметь средства, позволяющие добавлять и удалять предложения.

Системным предикатом, обеспечивающим доступ к программе, является предикат clause (Head, Body). К цели clause (Head, Body )? можно обращаться, если аргументу Head сопоставлено значение. Ищется первое предложение в программе, заголовок которого унифицируем с термом Head. Далее заголовок и тело этого правила унифицируются с аргументами Head и Body. При возврате каждое предложение унифицируемое с аргументами цели, дает одно решение. Отметим, что доступ к правилам, минуя заголовки, невозможен.

Считается, что телом факта является атом true. Конъюнктивная цель изображается с помощью бинарного функтора «,». Впрочем, можно легко абстрагироваться от конкретного представления.

Рассмотрим программу 3.12, задающую отношение member

member (Х,[X | Xs]).

membег(Х,[Y | Ys]) member(X,Ys).

Цель clause (member (X,YS), Body) имеет два решения: {Ys = [X | Xs},Body = true} и {Ys =[Y | Ys1],Body = member (X.Ys1)}. Отметим, что каждое выполнение унификации приводит к созданию новых копий переменных, входящих в предложение. В терминах металогических примитивов freeze и melt можно сказать, что предложения программы хранятся в замороженном виде. Каждое обращение к предложению clause вызывает размораживание ранее замороженного предложения. Описанный метод обращения является логическим аналогом повторной входимости в традиционном программировании.

Существуют системные предикаты, выполняющие добавление предложений к программе и удаление предложений из программы. Основной предикат для добавления предложений – assert (Clause), он присоединяет предложение Clause в качестве последнего предложения соответствующей процедуры. Например, решение цели assert (отец (аран, лот))? добавляет в программу факт отец. При добавлении правил следует вводить дополнительные скобки, чтобы учесть старшинство термов. Например, синтаксически правильным является выражение assert ((родитель (X, Y)omeu(X,Y))).

Имеется вариант предиката assert, а именно asserta, добавляющий предложение в начало процедуры.

Если аргументу Clause значение не сопоставлено (или если значение аргумента Clause имеет вид НВ, а переменной Н значение не сопоставлено), то возникает ошибочная ситуация.

Предикат retract (С) удаляет из программы первое предложение, унифицируемое с С. Заметим, что для удаления правила вида a b,с,d следует задать цель retract ((а С)). Обращение к предикату retract может только пометить удаляемое предложение, а не удалить его физически из программы. Реальное удаление может произойти только при решении вопроса верхнего уровня Пролога. Это объясняется методом реализации предиката и может привести к неправильным действиям в некоторых версиях Пролога.

Добавление предложения замораживает имеющиеся в предложении термы. Удаление того же предложения размораживает новые копии термов. Во многих версиях Пролога это используется в качестве простейшего способа копирования термов. В частности, предикат copy, введенный в гл. 10, может быть задан следующим правилом:

copy(X,Y) asserta($tmp(X)),retract ($tmp(Y)).

При этом предполагается, что функтор $tmp больше нигде в программе не используется.

Предикаты assert и retract вводят в Пролог возможность появления побочных эффектов при программировании. Программы, успешное выполнение которых зависит от побочных эффектов, трудны для чтения, отладки и формального анализа. Поэтому эти предикаты являются до некоторой степени спорными, и в ряде случаев их использование свидетельствует о лености ума или некомпетентности. При программировании следует по возможности реже прибегать к этим предикатам. Многие программы, приведенные в данной книге, могли быть записаны с использованием предикатов assert и retract, но при этом были бы потеряны ясность и эффективность. Более того, по мере совершенствования компиляторов с Пролога неэффективность применения предикатов assert и retract будет становиться все более очевидной.

Однако можно привести логическое обоснование некоторого ограниченного использования предикатов assert и retract. Например, добавление правила оправданно, если уже выяснено, что это правило является логическим следствием программы. Такое добавление не влияет на логическое значение программы, так как не позволяет выводить новые следствия, но может повлиять на эффективности поскольку некоторые следствия теперь могут быть выведены быстрее. Такт использование продемонстрировано в конструкции «лемма», описанной в разд. 12.3

Аналогично удаление правила обоснованно, если правило логически излишне. В этом случае удаление играет роль логической сборки мусора, предназначенной для уменьшения размеров программы.

Укажем некоторые другие примеры законного применения предикатов assert и retract. Одно из них состоит в установке и использовании глобальных переключателей, влияющих на выполнение программы. Это применение будет рассматриваться в разд. 13.2, посвященном программистским трюкам. Другое применение возникает при решении задач, которые по определению требуют модификации программы (например, программа consult в разд. 12.5 и такие метапрограммы, как редакторы).

Запоминающие функции

Запоминающие функции сохраняют промежуточные результаты с целью их использования в дальнейших вычислениях. Запоминание промежуточных результатов в чистом Прологе невозможно, поэтому реализация запоминающих функций основана на побочных эффектах. Такой способ программирования может быть назван программированием по восходящей методологии.

Исходной запоминающей функцией является функция lemma (Goal). Операционное понимание состоит в следующем: предпринимается попытка доказать цель Goal, и если попытка удалась, результат доказательства сохраняется в виде леммы. Отношение задается следующим образом;

lemma (Р) Р, asserta((P !)).

Следующая попытка доказать цель Р приведет к применению нового правила, что позволит избежать ненужного повторения вычислений. Отсечение введено для того, чтобы воспрепятствовать повторному рассмотрению всей программы. Применение отсечения обоснованно лишь в тех случаях, когда цель Р имеет не более одного решения.

Использование лемм демонстрируется на примере программы 12.3, описывающей решение задачи о ханойской башне. В ней радикально улучшены характеристики программы 3.30, решающей данную задачу. Хорошо известно, что решение задачи о ханойской башне с N дисками требует 2- 1 перемещений. Например, в случае 10 дисков требуются 1023 перемещения или, в терминах программы 3.30, требуются 1023 обращения к процедуре hanoi(l.A,B,C,Xs). Суммарное число общих вызовов процедуры hanoi/5 будет существенно больше.

Решение задачи о ханойской башне состоит в повторяющемся решении подзадач о перемещении одного и того же количества дисков. Запоминающая функция может быть использована при повторном обращении к поиску перемещений, необходимых для решения задачи с меньшим числом дисков. В последующих попытках решить ту же подзадачу могут использоваться уже вычисленные последовательности перемещений вместо их перевычисления.

Эта идея реализована в рекурсивном предложении процедуры Hanoi в программе 12.3. В первом обращении к процедуре hanoi решается задача с N – 1 диском. Это решение запоминается и может быть использовано при втором обращении к процедуре hanoi с N – 1 диском.

Программа проверяется с помощью предиката test_hanoi(N, Pegs, Moves). Здесь N число дисков. Pegs список названий трех стержней, Moves список перемещений, которые следует выполнить. Заметим что для эффективного использования запоминающих функций следует сначала решить общую задачу. Только после того, как общая задача полностью решена и запоминающие функции записали результат своей работы, можно задавать названия стержней.

hanoi(N,A,B,C,Moves)

Moves последовательность перемещений, необходимых для переноса N дисков со стержня А на стержень В с использованием промежуточного диска С. Перемещения производятся по правилам задачи о ханойской башне.

hanoi(l,A,B,C,[A to В]). hanoi(N,A,B,C,Moves) 

N > 1,

N1:= N - 1,

lemma(hanoi(N1,A,C,B,Ms1)),

hanoi (N1,C,B,A,Ms2),

append(Msl ,[A to B | Ms2],Moves).

lemma(P) P, asserta((P !)),

Проверка

test_.hanoi(N, Pegs, Moves) 

hanoi(N, A, B,C, Moves), Pegs = [A,B,C].

Программа 12.3. решение задачи о ханойской башне с использованием запоминающих функций.

Интерактивные программы

Наиболее распространенный случай использования побочного эффекта – интерактивный цикл. С терминала читается некоторая команда, выполняются соответствующие действия и затем читается следующая команда, В обычных языках программирования интерактивный цикл чаще всего реализуется с помощью цикла while. Программа 12.4 описывает общую структуру подобных программ. В данной программе введенная команда повторяется путем вывода на экран.

echo read (X), echo (X).

echo(X) end_of_ftle(X),

echo(X) write (X), nl, read(Y),!, echo(Y).

Программа 12.4. Основной интерактивный цикл.

Цикл чтение-ответ запускается с помощью цели echo. Основу программы составляет отношение echo(X), где Х – терм, который следует вывести на экран. Программа использует предикат end_of_fille (X), который выполнен, если Xсимвол конца файла. Конретный вид этого символа зависит от операционной системы. Если обнаружен конец файла, то цикл завершается. В противном случае терм выводится на экран и считывается новый терм.

Заметим, что чтение терма и проверка терма выполняются раздельно. Это необходимо для того, чтобы избежать потери терма, так как терм не может быть прочитан повторно. С такой же ситуацией мы встретились в программе 12.2 при обработке литер. Литера сначала читалась, а уж потом отдельно обрабатывалась.

Программа 12.4 является итерационной и детерминированной. Если система включает в себя оптимизацию остатка рекурсии, то программа может эффективно выполняться, всегда используя один и тот же небольшой объем памяти.

Мы приведем два примера программ, использующих основной цикл чтения и последующей обработки термов. Первый пример – редактор строк. Вторым примером является интерактивная программа, представляющая собой оболочку, обрабатывающую команды Пролога. По существу она является написанным на Прологе интерпретатором верхнего уровня для Пролога.

Первое решение, которое следует принять при создании редактора строк на Прологе, – выбор представления файла. Необходимо иметь доступ к каждой строке файла и к позиции курсора, соответствующей текущей позиции редактируемого файла. Мы используем структуру file (Before, After), где Before список строк перед курсором; After список строк, расположенных после курсора. Курсор может размещаться только в конце некоторой строки. Строки, расположенные перед курсором, размещены в обратном порядке, что облегчает доступ к строкам, ближайшим к курсору. В основном цикле с клавиатуры вводится команда, с помощью которой строится новая версия файла. Редактор записан в виде программы 12.5.

edit edit(file([ ],[ ])).

edit (File) 

write_prompt, read(Command), edit (File, Command).

edit (File, exit) !.

edit (File, Command) 

apply (Command, File, Fiiel),!, edit(FiIel).

edit (File, Command) 

writeln (Command, ‘is not applicable'l),

!,

edit(FiIe).

apply(up, file([X | Xs], Ys),file(Xs, [X | Ys])). apply(up(N),file(Xs, Ys),file(XsI, Ysl)

N >0,up(N, Xs, Ys, Xsl, Ysl).

apply(down, file(Xs,[Y | Ys]),fi1e([Y [ Xs],Ys)),

apply(insert(Line),file(Xs, Ys),file(Xs,[Line | Ys]}).

apply (delete, file(Xs,[Y [ Ys],file(Xs, Ys)),

apply(print, file([X | Xs],Ys),file([X | Xs],Ys))

write(X),nl.

apply(print(*),file(Xs, Ys),file(Xs,Ys))

reverse(Xs, Xs1), write_file(Xs1), write_file(Ys).

up(N,[ ],Ys,[ ],Ys).

up(0,Xs,Ys,Xs,Ys).

up(N,[X | Xs],Ys, Xsl, Ysl)

N>0,N1 is N- l,up(Nl, Xs,[X | Ys],Xs1,Ysl).

Write_filе([Х | Xs])

write(X), nl, write_file(Xs). write, file([ ]).

write prompt write(‘»'), nl.

Программа 12.5. Редактор строк.

Процесс редактирования начинается обращением к процедуре edit, которая использует цель file([ ], [ ]) для задания пустого файла в качестве начального значения редактируемого файла. Интерактивный цикл начинается с помощью процедуры edit(File). Решение цели edit_prompt приводит к выводу подсказки на экран, далее читается и выполняется команда. При выполнении используется основной предикат – edit(File, Command). Этот предикат применяет команду Command к файлу File. Применение команды осуществляется в процессе решения цели apply (Command, File, File1), где File1 – новая версия файла, полученная после применения команды. Редактирование продолжается путем обращения к процедуре edit/1 с аргументом File1. Третье предложение процедуры edit/2 используется в тех случаях, когда команда неприменима, т. е. в тех случаях, когда цель apply не выполнена. В этих случаях на экран выводится соответствующее сообщение и редактирование продолжается. Процесс радактирования завершается по команде exit, анализируемой в отдельном предложении процедуры edit/2.

Давайте рассмотрим несколько предложений процедуры apply, чтобы представить себе, как же происходит анализ команд. Наиболее простыми являются команды перемещения курсора. В предложении

арр1у(up,filе([Х | Xs],Ys), file(Xs,[X, Ys])).

указано, что перемещение курсора вверх состоит в перемещении строки, находящейся непосредственно перед курсором, на место строки, находящейся непосредственно за курсором. Команда не выполнится, если курсор находится в начале файла. Команда перемещения курсора вниз аналогична команде перемещения курсора вверх и также описана в программе 12.5.

Перемещение курсора вверх на N строк, а не на одну строку производится с помощью вспомогательного предиката ир15, изменяющего позицию курсора в файле. Правильность выполнения данного предиката легко усматривается из его определения. Заметим, что в процедуре apply перед обращением к процедуре up проверяется, допустимы ли аргументы процедуры up, т. е. является ли число строк положительным. В самом предикате up учитывается то, что число строк, на которое требуется переместить курсор, может быть больше числа строк в файле. В этом случае выполнение команды приводит к установке курсора в начало файла. Задача перемещения курсора на N строк вниз рассматривается в упражнениях.

Другие команды программы 12.5 вставляют и удаляют строки. Команда вставки insert (Line) содержит один аргумент, а именно вставляемую строку. Команда удаления выполняется непосредственно. Она неприменима, если курсор находится в конце файла. В редакторе имеются также команды печати строки, находящейся под курсором, – команда print и команда печати всего файла – print (*).

Команды редактора – взаимоисключающие. Для любой команды редактора применимо лишь одно правило процедуры apply. Это указывается с помощью отсечения во втором предложении процедуры edit/2. После выполнения цели apply путь вычисления однозначно определен. Такой способ указания детерминизма несколько отличается от описанного в разд. 11.1, где отсечения требуется применять непосредственно к самим фактам процедуры apply. Различие между этими двумя подходами носит скорее косметический характер.

Можно усовершенствовать редактор, связав с каждой командой собственное сообщение об ошибке. Предположим, например, что при попытке переместиться вверх из начала файла желательно получать более содержательное сообщение, чем «команда неприменима». Это можно сделать за счет усложнения предложения процедуры apply, относящегося к перемещению в файле вверх.

Перейдем от редактора к оболочкам. Оболочка получает команды с терминала и выполняет их. В качестве примера рассмотрим оболочку, решающую цели на Прологе. Она описана в программе 12.6.

shell 

shell_prompt, read(Goal), shell(Goal). shell(exit) !

shell (Goal)

ground (Goal),!, shell_solve_ground(Goal), shell.

shell(Goal) 

shell_solve(Goal), shell.

shell_solve(Goal) 

Goal, write(Goal), nl, fail. shell_solve(Goal) 

write ('Решений (больше) нет'), nl.

sheiLsolve_ground(Goal) 

Goal,!, write ('Да'), nl.

shell_solve_ground (Goal)

write('Heт’), nl.

shelLprompt  wnte(‘Cледующая команда?').

Программа 12.6. Интерактивная оболочка.

Процесс работы начинается с помощью решения цели shell. Начало работы подобно началу редактирования. С помощью процедуры shell_prompt выводится подсказка, потом читается цель и предпринимается попытка решить эту цель с помощью обращения к процедуре shell (Goal). Решение основных целей, дающее ответ да/нет, отличается от решения неосновных целей, дающего в качестве ответа цель, в которую подставлены соответствующие значения. Эти два случая рассматриваются соответственно в процедурах shell_solve_ground и shell_solve. Оболочка прекращает работу при вводе цели exit.

И shell_solve_ground, и shell_solve используют доступность метапеременных для обращения к решаемой цели. Успешность или безуспешность решения цели определяют вид выдаваемого сообщения. Эти два предиката являются простейшими примерами метаинтерпретаторов - предмета обсуждения гл. 19.

Процедура shell_solve содержит интересную конструкцию «решение-вывод-fail», полезную при выявлении всех решений цели путем принудительного возврата. Для того чтобы избежать безуспешных вычислений, добавлено еще одно предложение. Оно выполняется, когда исчерпаны все решения цели. Любопытно отметить, что невозможно непосредственно получить все решения целей, не прибегая к некоторой форме побочного эффекта. Причина этого явления разъясняется дальше в лекции, посвященной программированию второго порядка.

На основе данной оболочки может быть создано средство хранения протокола сеанса работы с Прологом. Такая система описана в программе 12.7. Эта новая оболочка начинает работать с вызова цели log, которая обращается к основному интерактивному предикату shell (Flag), аргумент Flag вначале равен константе log. Переключатель Flag принимает одно из двух значений log или nolog; это значение определяет, следует ли результат текущей работы заносить в протокол.

Данная программа является усовершенствованием программы 12.6. Основное отличие заключается в добавлении в главные предикаты дополнительного аргумента, значение которого определяет, включен ли режим хранения. Добавлены две дополнительные команды – log и поlog для включения и выключения режима хранения.

Переключатель используется в предикатах, относящихся к вводу-выводу. Каждое сообщение, выдаваемое на экран, должно быть также занесено в файл протокола. Аналогично каждая вводимая цель заносится в протокол, что облегчает чтение

log shell(log).

shell(Flag) 

shell_prompt, shell_read (Goal, Flag),shell(Goal,Flag)

shell (exit, Flag)

!, close_logging_file.

shell (nolog. Flag)

!, shell (nolog).

shell(log. Flag)

!, shell(log).

shell (Goal, Flag) 

ground(Goal),!, shell_solve_ground (Goal, Flag),shell(Flag).

shell(Goal,Flag)

shell_solve(Goal, Flag), shell (Flag).

shell_solve(Goal, Flag)-

Goal, shell_write(Goal,Flag), nl, fail.

shell_solve(Goal) 

shell_write ('Решений (больше) нет', Flag), nl.

shell_solve_ground (Goal, Flag) 

Goal,!, shell, write ('Да', Flag), nl. shell_solve_ground (Goal, Flag) 

shell_write(‘Heт', Flag), nl.

shelLprompt  write('Cлeдyющaя команда ?').

shelLread (X, log) read (X),

file_write(['Следующая команда ?',Х], 'prolog,log'). shelLread (X, nolog)  read(X).

shelLwrite (X, nolog)  write(X).

shelLwrite (X, log)  write(X),file_write([X],prolog.log').

ffle„write(X,File)

telling(Old), tell(File). writeln(X),nl,tell(O1d).

close logging_file tell('prolog.log'), told.

Программа 12.7. Реализация средства хранения протокола сеанса.

протокола. Поэтому обращение к предикату read имевшееся в программе 12.6, заменено на обращение к процедуре shell_read, а обращение к write заменено на обращение к shell_write.

Определение предиката shell_write описывает наши действия:

shell_write (X, nolog)  write (X).

shellwrite (X, log) write(X), file_write([X],’prolog.log').

Если текущее значение переключателя – nolog, то результат просто выводится на экран. Если значение – log, то дополнительная копия результата помещается в файл prolog.log. Предикат file_write(X,File) записывает строку Х в файл File.

В программе 12.7 – только два системозависимых предиката file_write и close_ logging_file. Они зависят от дополнительных системных предикатов, обрабатывающих файлы. Их определение использует примитивы Edinburgh-Пролога tell. told и telling, описанные в приложении Б. Еще одно соглашение, принятое в тексте программы – результирующий протокол записывается в файл prolog.log.

Циклы, управляемые отказом

Интерактивные программы, описанные в предыдущем разделе, были основаны на неминимальных рекурсивных циклах. Существует другой способ записи циклов в Прологе, аналогичный записи циклов repeal в обычных языках программирования, Выполнение таких циклов управляется с помощью отказов, поэтому их называют циклами, управляемыми отказами. Такие циклы бывают полезны лишь при совместном использовании с внелогическими предикатами, имеющими побочный эффект. Работа таких циклов может быть понята только с операционной точки зрения.

Простым примером цикла, управляемого отказом, служит вопрос Goal, write (Goal), nl, fail?, который приводит к выводу всех решений цели goal на экран. Такой цикл использован в оболочках, описанных в программах 12.6 и 12.7.

Цикл, управляемый отказом, может быть использован при определении системного предиката tab(N), предназначенного для вывода на экран N пробелов. В нем используется отношение between, описанное в программе 8.5:

tab (N) between (1,N,I), put(32), fail.

Каждая интерактивная программа предыдущего раздела может быть переписана с использованием циклов, управляемых отказом. Новый вариант основного интерактивного цикла приведен в программе 12.8. Он основан на незавершающемся системном предикате repeat, который может быть задан с помощью минимальной рекурсивной процедуры, приведенной в программе 12.8. В отличие от программы 12.4 в данной программе решение цели echo(X) приводит к безуспешному вычислению, если только Х не символ конца файла. Безуспешное вычисление вызывает возврат к цели repeat, цель выполняется, считывается и затем выводится на экран следующий терм. Отсечение в определении предиката echo гарантирует от позднейшего повторения цикла repeat.

echo repeat, read(X), echo(X),!.

echo(X) end_of_file(Х),!.

echo(X) write(X), nl, fail.

repeat.

repeat repeat.

Программа 12.8. Основной интерактивный цикл repeat.

Цикл, управляемый отказом и использующий предикат repeat, имеет название repeat-цикл.Такие циклы аналогичны циклам вида repeat в обычных языках программирования. Repeat-циклы применяются в Прологе для описания интерактивного взаимодействия с внешней системой путем повторяющегося ввода и (или) вывода. В repeat-цикле обязательно должен присутствовать предикат, гарантированно приводящий к безуспешным вычислениям (в программе 12.8 это – цель echo(Х)), определяющим продолжение итерации. Такой предикат вычисляется успешно лишь в момент выхода из цикла. Можно сформулировать полезное эвристическое правило построения repeat-циклов: в теле правила, содержащего цель repeat, должно быть отсечение, предохраняющее от незавершающихся вычислений при возврате в цикл.

Мы используем repeat-цикл для определения системного предиката consult(File), предназначенного для чтения и последующего добавления к программе предложений из некоторого файла. Определение приведено в программе 12.9. Системные предикаты see(File) и seen используются соответственно для открытия и закрытия входного файла.

consult(File) see(File), consult_loop, seen

consult coop 

repeat, read(Clause), process(Clause),!.

process (X) 

end_of_file(X),!. process(Clause) 

assert (Clause), fail,

Программа 12.9. Обращение к файлу.

Рекурсивные циклы предпочтительнее repeat-циклов, поскольку последние не имеют логической интерпретации. На практике, однако, repeat-циклы часто необходимы при выполнении большого объема вычислений, особенно в реализациях Пролога без оптимизации остатка рекурсии и (или) без сборки мусора. Обычно явный отказ приводит к требованию некоторого зависящего от реализации объема памяти.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]