• 5.1. Ввод и вывод термов
  • 5.1.1. Вывод термов
  • 5.1.2. Ввод термов
  • 5.2. Ввод и вывод литер
  • 5.2.1. Вывод литер
  • 5.2.2. Ввод литер
  • 5.3. Ввод предложений
  • 5.4. Чтение файлов и запись в файлы
  • 5.4.1. Запись в файлы
  • 5.4.2. Чтение файлов
  • 5.4.3. Ввод программ
  • 5.5. Объявление операторов
  • ГЛАВА 5 ВВОД И ВЫВОД

    В предыдущих главах фигурировал только один способ предоставления информации Пролог-программе – обращение к ней с вопросом. Точно так же единственный способ определить значение переменной на некотором этапе доказательства согласованности целевого утверждения с базой данных состоял в построении вопроса таким образом, чтобы Пролог-система напечатала ответ в виде «Х=ответ». В большинстве случаев такого непосредственного взаимодействия с программой посредством вопросов вполне достаточно, чтобы убедиться в том, что программа работает правильно. Однако во многих ситуациях удобно писать программу на Прологе так, чтобы она сама инициировала диалог с пользователем. Например, предположим, что имеется база данных, содержащая информацию о событиях, происходивших в мире в 16-м веке. Информация представлена в виде фактов, включающих дату события и его краткое содержание. Даты могут быть представлены как целые числа, а содержание – в виде списков атомов. Те атомы в списке, которые начинаются с прописной буквы, будут заключаться в одинарные кавычки, чтобы Пролог не принял их за переменные:


    событие(1505, ['Начала','Евклида', переведены, на, латинский, язык]).

    событие(1510, ['Начало', спора, между, 'Реучлином', и 'Пфефферкорном']).

     событие(1523, [Кристиан, 'II', покинул, 'Данию']).

    . . .


    Теперь, для того чтобы узнать, что связано с конкретной датой, мы могли бы задать следующий вопрос:


    ?- событие(1505,Х).


    на что Пролог напечатал бы ответ:


    Х=['Начала', 'Евклида', переведены, на, латинский, язык]


    Представление краткого содержания событий в виде списков атомов дает возможность определить дату событий по некоторым ключевым моментам, имевшим место. Например, рассмотрим предикат когда, который мы определим ниже. Целевое утверждение когда(Х, Y) доказуемо, если в заголовке события, имевшего место в году Y, упоминается X:


    когда(Х,Y):- событие(Y,Z), принадлежит (X,Z).

    ?- когда(Кристиан,D).

    D=1523


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


    событие(1511, "Лютер посещает Рим").

    событие(1521, "Генри III провозглашен защитником веры").

    событие(1524, "Умер Васко да Гама").

    событие(1529, "Берквин сожжен в Париже").

    событие(1540, "Возобновление войны с Турцией").

    . . .


    Такая форма представления удобнее для ввода, но посмотрим, что произойдет, если задаться вопросом


    ?- событие(1524,X).


    В ответ Пролог напечатает непонятный список кодов ASCII, соответствующих литерам строки, являющейся значением переменной X! Хотя список литер легче ввести в систему, механизм 'вопрос – ответ' Пролога не позволяет получить ясный ответ. Было бы намного удобнее, если бы вместо того, чтобы обращаться к Прологу с подобными вопросами, можно было написать программу, которая вначале спрашивает, какая дата вас интересует, а затем выводит содержание соответствующего события на терминал. При этом названия событий можно было бы представлять в желаемом виде. Для выполнения задач подобного сорта в Прологе существует ряд встроенных предикатов, которые печатают свои аргументы на терминале. Имеются также предикаты, которые ожидают, пока пользователь введет текст с клавиатуры терминала, и присваивают переменной в качестве значения введенный текст. С помощью этих предикатов программа может взаимодействовать с вами, принимая от вас данные и печатая для вас результат. Когда программа ждет от вас данные, будем говорить, что она читает или вводит данные. Точно так же, когда программа печатает некоторый результат, будем говорить, что она выводит результат. В этой главе мы описываем различные методы ввода и вывода данных. Один из рассматриваемых примеров связан с печатью кратких содержаний событий из базы данных исторических событий, а в заключение будет приведена программа, воспринимающая предложения на естественном языке и преобразующая их в список констант, который впоследствии может быть подвергнут обработке другими программами. Эта преобразующая программа, названная ввести, может использоваться как некий «модуль», с помощью которого можно создавать программы для анализа предложений на естественном языке. Программы, выполняющие такой анализ, обсуждаются в последующих главах, особенно в гл. 9.

    5.1. Ввод и вывод термов

    5.1.1. Вывод термов

    Наиболее удобный способ напечатать некоторый терм на дисплее терминала состоит, по-видимому, в использовании встроенного предиката write. Если значением переменной X является терм, то появление цели write(X) вызовет печать этого терма на дисплее. В случае если переменная X неконкретизирована, будет напечатано некоторое уникальное имя, которое состоит из одних цифр (например, '_253'). Однако если две переменные «сцеплены» в пределах одного и того же аргумента предиката write, то им будет соответствовать одна и та же переменная. Предикат write нельзя согласовать вновь. Этот предикат выполняется лишь один раз, и всякая попытка вновь согласовать его заканчивается неудачей. Нельзя ли использовать write для вывода краткого содержания исторических событий в нашем примере? Вспомните, что строка литер в действительности представляется как список кодов литер. Если бы такой список был выведен с помощью предиката write, то он был бы напечатан как заключенная в квадратные скобки последовательность целых чисел, разделенных запятыми!

    Прежде чем мы познакомимся с первым примером использования предиката write, нам нужно описать еще два предиката. Встроенный предикат nl применяется для перехода на новую строку при печати данных на дисплее. Название «nl» образовано от «new line» (новая строка). Как и write, предикат nl выполняется только один раз. Следующий встроенный предикат tab используется для печати пробелов на экране дисплея. Целевое утверждение tab(X) выполняется только раз и вызывает перемещение курсора на X позиций вправо. Предполагается, что значение переменной X – целое число. Возможно, выбор имени tab не очень удачен, так как в действительности этот предикат не имеет ничего общего с табуляцией на обычных пишущих машинках или на дисплеях терминалов.

    При печати списков полезно печатать элементы списка таким образом, чтобы получаемый результат можно было легко понять. Списки, которые содержат другие «вложенные» списки, читать особенно трудно, тем более когда внутри них содержатся структуры. Определим предикат рр (pretty print – «хорошая печать») так, что целевое утверждение рр(Х, Y) печатает в удобном виде список, присвоенный в качестве значения переменной X. Смысл второго аргумента предиката рр будет объяснен позднее. Каждый автор программы, реализующей хорошую печать, имеет свой собственный стиль представления списков. Мы воспользуемся методом, при котором элементы списка печатаются в колонку. Если элемент сам является списком, то его элементы печатаются в колонке, которая смещена вправо по отношению к основной колонке. Такая форма представления по существу совпадает с рассмотренным в гл. 3 способом изображения списков. Например, список [1,2,3] «хорошо» печатается в следующем виде:

    1

    2

    3

    а список [1,2,[3,4],5,6] печатается как

    1

    2

       3

       4

    5

    6

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


    pp([H|T],I):-!, F is I+3, pp(H,F), ppx(T,F),nl.

    pp(X,I):- tab (I), write(X), nl.

    ppx([],_).

    ppx([H|T],I):- pp(H,I), ppx(T,I).


    Теперь видно, что второй аргумент предиката рр выполняет функции счетчика колонок. Целевое утверждение «верхнего уровня» для печати некоторого списка могло бы выглядеть как


    … PP(L,0),…


    при этом начальное значение счетчика колонок устанавливается равным 0. Первое утверждение предиката рр обрабатывает специальный случай – когда первый аргумент является списком. Если это так, то необходимо установить новую колонку, увеличив счетчик на некоторое число (здесь 3). Затем мы должны отпечатать с помощью рр голову списка, так как она сама может оказаться списком. Далее нужно напечатать все элементы хвоста списка, располагая каждый элемент в той же самой колонке. Это как раз и выполняет предикат ррх. А предикат ррх использует рр, поскольку каждый элемент может быть списком. Второе утверждение предиката рр соответствует случаю, когда нам необходимо напечатать что-либо, не являющееся списком. Мы просто делаем отступ на указанное число позиций, используем предикат write для печати терма и nl для перехода на новую строку. Первое утверждение для рр также заканчивается nl, поскольку печать каждого списка должна завершиться переходом на новую строку.

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


    рр(Х,I):- not(список(Х)), tab(I), write(X), nl.

    pp([H|T],I):- J is I+3, pp(H,J), ppx(T,J), nl.

    /*ppx как и ранее */

    список([]) список([_|_]).


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

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


    phh([]):- nl.

    phh([H|T]):- write(H), tab(l), phh(T).


    Так, при следующем запросе было бы напечатано каждое событие, в содержании которого встречается «Англия»:


    ?- событие(_,L), принадлежит('Англия',L), phh(L).


    Обратим внимание на использование механизма возврата для поиска в базе данных. Каждый раз, когда для целевого утверждения принадлежит не находится сопоставление, делается попытка найти новое сопоставление для целевого утверждения событие. В результате в поисках событий, в которых упоминается атом «Англия», будет целиком просмотрена сверху вниз вся база данных.

    Предикат write печатает термы с некоторым «пониманием» того, что он делает, так как он учитывает, какие объявления операторов были сделаны. Например, если мы объявили некоторый атом как инфиксный оператор, то терм, имеющий этот атом в качестве функтора структуры с двумя аргументами, будет напечатан таким образом, что атом окажется между аргументами. Существует еще один предикат, который выполняет те же действия, что и write, за тем исключением, что он игнорирует все сделанные объявления операторов. Этот предикат называется display. Различие между write и display иллюстрирует следующий пример:


    ?- write(a+b*c*c),nl, display(a+b*c*c).

    a+b*c*c

    +(a,*(*(b,c),c))

    да


    Обратим внимание на то, что предикат display обработал атомы + и * – точно так же, как и любые другие атомы, которые он печатает в этом терме. Как правило, нежелательно, чтобы печатаемые структуры выглядели подобным образом, так как наличие операторов обычно делает более понятными при чтении как вводимые, так и выводимые программой данные. Однако иногда, когда мы не совсем уверены в том, что знаем, каков приоритет операторов, использование предиката display может оказаться очень полезным.

    5.1.2. Ввод термов

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

    Используя предикаты read и phh, как они были определены выше, мы можем написать программу для печати краткого содержания исторических событий, хранящихся в базе данных, с помощью фактов с предикатом событие. Эта программа имеет вид:


    обращение:-

     phh(['Какая',дата,вас,'интересует?'!),read(D),coбытиe(D,S),pph(S).


    Мы определили предикат обращение, не имеющий аргументов. Когда мы обращаемся к системе с вопросом


    ?- обращение.


    Пролог напечатает


    Какая дата вас интересует?

     и будет ждать ответа. Предположим, что мы ввели с клавиатуры

    1523.


    Обратите внимание на то, что после 1523 необходимо ввести точку, так как этого требует предикат read. И, как обычно, мы должны нажать клавишу RETURN, чтобы сообщить ЭВМ, что мы закончили ввод строки текста. После этого Пролог ответит


    Кристиан II покинул Данию


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

    5.2. Ввод и вывод литер

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

    5.2.1. Вывод литер

    Если переменная X имеет в качестве значения некоторую литеру (ее код ASCII), то эта литера будет напечатана при обработке целевого утверждения put(X). Предикат put всегда выполняется и не может быть пересогласован (это приводит к неудаче). В качестве «побочного эффекта» put печатает литеру на дисплее терминала. Например, мы можем напечатать слово hello довольно необычным способом:


    ?- put(104),put(101),put(108),put(108),put(111).

    hello


    Результатом такой конъюнкции целей является печать Прологом литер h, е, l, l, о непосредственно под вопросом, как показано выше. Мы уже видели, что имеется возможность начать печать текста с начала следующей строки, использовав для этого предикат без аргументов nl. В действительности nl «печатает» некоторые управляющие литеры, что вызывает перевод курсора на дисплее терминала на начало следующей строки. Вопрос


    ?- put(104),put(105),nl,put(116),put(104),put(101),put(114), put(1O1).


    вызвал бы следующую печать:


    hi

    there


    Другой предикат, с которым мы уже познакомились, - это tab(X), печатающий X пробелов (ASCII код равен 32). Разумеется, переменной X должно быть присвоено целое число. Отметим, что предикат tab(X) мог бы быть определен так:


    tab(0):- !.

    tab(N):- put(32), M is N-1, tab(M).


    Теперь мы можем определить предикат, который мы назовем печать_строки. Если значением переменной X является список кодов литер (строка), то целевое утверждение печать_строки напечатает этот список (строку), используя put для печати каждого элемента списка. Как и во всех подобных программах, граничным условием является появление пустого списка. Это условие мы и используем для завершения рекурсии. При непустом списке с помощью put печатается голова списка, а затем используем печать_строки – хвост списка:


    печать_строки([]).

    печать_строки([Н|Т]):- put(H), печать_строки(Т).

    ?- печать_строки(«Чарлз V отрекся от престола в Брюсселе»).

    Чарлз V отрекся от престола в Брюсселе


    Если мы решили представлять краткое содержание исторических событий как строки литер, а не как списки атомов, то такого определения вполне достаточно, чтобы печатать строки из базы данных для предиката событие.

    5.2.2. Ввод литер

    Для ввода литер, набираемых на клавиатуре терминала, могут быть использованы предикаты get0(X) и get(X). Эти предикаты всегда согласуются с базой данных, если их аргументы неконкре-тизированы, а попытка повторного согласования всегда неудачна. При обработке целей, включающих эти предикаты, ЭВМ ожидает до тех пор, пока пользователь не наберет на клавиатуре какую-либо литеру. Указанные предикаты немного различаются тем, что get0(X) присвоит X любую набранную на клавиатуре литеру независимо от ее вида. Напротив, get(X) пропустит все управляющие литеры и присвоит X в качестве значения первую печатаемую литеру. Как отмечалось в гл. 2, печатаемая литера – это литера, которая визуализируется на дисплее терминала.

    Если X уже присвоено значение, то целевое утверждение get(X) пропустит все управляющие литеры и сравнит следующую за ними печатаемую литеру со значением X. Доказательство согласованности целевого утверждения зависит от результата этого сравнения. Целевое утверждение get0(X) сравнивает X со следующей литерой и в зависимости от совпадения считается согласованным с базой данных или нет.

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

    5.3. Ввод предложений

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

    , ; : ? ! .

    будут рассматриваться как отдельные слова. Все другие литеры являются разделителями между словами. Предложение считается законченным, когда встречается одно из слов '.', '!' или '?'. Прописные буквы автоматически преобразуются в строчные, так что одно и то же слово всегда превращается в один и тот же атом. В результате такого определения программа будет поддерживать диалог с пользователем, подобный следующему:

    ?- ввести(S).

    The man, who is very rich, saw John's watch.

    S = [the,man,',',who,is,very,rich,',',saw,'John's',watch,'.']

    В действительности мы вставили в представление предложения дополнительные одинарные кавычки, чтобы выделить некоторые атомы.

    Программа использует предикат get0 для ввода литер с терминала. Затруднение, связанное с предикатом get0, состоит в том, что если литера прочитана с терминала этим предикатом, то она «ушла навсегда» и никакое другое целевое утверждение get0 или попытка вновь доказать целевое утверждение get0 не позволит получить доступ к этой литере вновь. Поэтому следует избегать возврата за точку использования get0, если мы хотим избежать «потери» литеры, которую он читает. Например, следующая программа, которая должна вводить литеры и печатать их снова, заменяя литеры а на b (код литеры 97 на код 98), не будет работать:


    выполнить:- заменить_литеру, выполнить.

    заменить_литеру:- get0(X) = 97,!, put(98).

    заменить_литеру:- get0(X), put(X).


    Приведенную программу в любом случае нельзя считать хорошей, потому что она будет работать вечно. Однако рассмотрим эффект попытки доказать согласованность целевого утверждения заме-нить_литеру. Если первое правило определения предиката заме-нить_литеру используется для чтения литеры, код которой отличен от 97, то возврат приведет к тому, что будет сделана попытка воспользоваться вместо него вторым правилом. Однако согласование целевого утверждения get0(X) во втором правиле приведет к тому, что X будет конкретизирована следующей литерой. Это объясняется тем, что доказательство исходного целевого утверждения get0 было необратимым процессом. Таким образом, эта программа в действительности не печатала бы все литеры. Она даже иногда печатала бы литеры а.

    Как же программа ввести преодолеет проблемы возврата при вводе? Ответ заключается в том, что программа конструируется таким образом, что она вводит литеры с опережением на одну литеру, а проверки литеры выполняются правилом, отличным от правила, в котором эта литера была прочитана. Если литера введена в каком-то месте программы и не может быть здесь же использована, то она возвращается обратно для возможного использования другими правилами. В соответствии со сказанным предикат для ввода одного слова читать_слово в действительности имеет три аргумента. Первый предназначен для литеры, которая была получена при последнем выполнении get0 где-либо в программе, но которую оказалось невозможным использовать в месте ее получения. Второй предназначен для атома, который будет создан для прочитанного слова. Последний аргумент предназначен для литеры, следующей во вводимом предложении сразу за прочитанным словом. Для того чтобы определить, где кончается слово, необходимо ввести литеру, следующую непосредственно за словом. Эта литера должна быть сохранена, потому что она может оказаться первой литерой другого слова.

    Здесь приведен текст программы:


    /* Прочитать предложение */

    ввести([Сл|Слс]):- get0(C), читать_слово(С,Сл,С1), остаток_предложения(Сл, С1, Слс).

    /* Дано слово и литера после него, ввести остаток предложения */

    остаток_предложения (Сл,_,П):- последнее_слово (Сл),!.

    остаток_предложения(Сл,С,[Сл1|Слс]):- читать_слово(С, Сл, С1), остаток_предложения(Сл1,С1,Слс).

    /* Ввести одно слово, имея начальную литеру и запомнив, какая литера идет после слова */

    читать_слово(С,Сл,С1):- литера(С),!, name(Сл,С), get0(C1).

    читать_слово(С,Сл,С2):- слово(С, Нс),!,get0(Cl),

    остаток_слова(С1,Сс,С2),name(Сл,[Нс|Сс]).

    читать_слово(С,Сл,С2):-get0(Cl), читать_слово (С1, Сл,С2).

    остаток_слова(С,[Нс|Сс],С2):-слово(С,Нс),!,get0(Cl),остаток_слова (С1, Сс, С2). остаток_слова(С, [],С).

    /* Эти литеры образуют отдельные слова */

    литера(44) /*, */

    литера(59) /*; */

    литера(58) /*: */

    литера(63) /*? */

    литера(ЗЗ) /*! */

    литера(46) /*. */

    /* Следующие литеры могут встретиться внутри слова */

    /* Второй факт для предиката слово преобразует прописные литеры в строчные

    слово(С,С):- С › 96, С ‹ 123. /* a b… */

    слово(С,М):- С › 64, С ‹ 91, M is С+ 32. /*А В… */

    слово(С,С):- С › 47, С ‹ 58 /* 1 2… 9*/

    слово(39,39). /* ' */

    слово(45,45) /* – */

    /* Следующие слова заканчивают предложение */

    последнее_слово('.').

    последнее_слово('!').

    последнее_слово('?').

    Упражнение 5.1. Объясните, для чего используется каждая переменная в приведенной программе.

    Упражнение 5.2. Напишите программу, которая читает неограниченную последовательность литер и печатает ее, предварительно заменяя вхождения литеры а литерой b.

    5.4. Чтение файлов и запись в файлы

    Предикаты, обсуждавшиеся в этой главе ранее, использовались для ввода (чтения) и вывода (записи) данных при обмене лишь с терминалом, но они могут быть использованы и в более общих ситуациях. В Пролог-системе определяется текущий входной поток данных, из которого производится чтение всех вводимых данных. Все выводимые данные записываются в текущий выходной поток данных. В обычном состоянии текущий входной поток данных поступает с клавиатуры терминала, а текущий выходной поток данных направляется на дисплей терминала. Часто оказывается удобно выполнять операции чтения и записи данных над файлами, которые представляют последовательность литер во вторичной внешней памяти. Конкретный вид этой памяти зависит от используемой ЭВМ, но сейчас файлы обычно хранятся на магнитных дисках. Предполагается, что каждый файл имеет собственное имя(имя файла), используемое для идентификации файла. Для того чтобы содержание этой главы было понятно, читателю следует познакомиться с правилами организации и способом задания имен файлов в той операционной обстановке, в которой он работает. В Прологе имена файлов представляются атомами, но мы не можем исключить возможность каких-либо ограничений на синтаксис имен файлов, накладываемых конкретной обстановкой, в которой работает Пролог-система.

    Файлы имеют определенную длину. Это означает, что они содержат определенное количество литер. В конце файла имеется специальный маркер, называемый маркером конца файла. Мы не обсуждали маркер конца файла до сих пор, так как выход на конец файла является более обычным делом для файлов, расположенных во внешней памяти, чем при обмене с терминалом. Если программа производит чтение файла, то маркер конца файла может быть обнаружен и в случае, когда программа читает термы и когда читаются отдельные литеры. Если при выполнении get0(X) встречается конец файла, то X будет конкретизирована некоторой управляющей литерой, обычно имеющей код 26 в таблице кодов ASCII. Если конец файла встречается при выполнении read(X), то X будет конкретизирована некоторым специальным термом, значение которого зависит от конкретной Пролог-системы. При попытке прочитать файл далее маркера конца возникает ошибка.

    Имеется встроенный (стандартный) файл, называемый user. Чтение этого файла вызывает ввод данных с клавиатуры терминала, а запись в этот файл приводит к тому, что литеры печатаются на дисплее. Такой режим работы считается стандартным. При вводе с клавиатуры терминала признак конца файла генерируется при вводе управляющей литеры с кодом ASCII 26. Это окажет на выполнение get0 и read такое же воздействие, как если бы встретился конец файла.

    5.4.1. Запись в файлы

    Для записи в файлы термов и литер могут быть использованы в точности те же самые предикаты, что обсуждались ранее Единственное различие состоит в том, что когда мы хотим записать данные в файл, то нам надо сменить текущий выходной поток данных так, чтобы им являлся файл, в который мы будем записывать данные, а не дисплей терминала. Текущий выходной поток данных изменяется с помощью предиката tell. Если X конкретизирована именем файла, которое должно быть атомом, то целевое утверждение tell(X) переключает текущий выходной поток данных таким образом, что любая операция записи (выполняемая с помощью write, put и других подобных предикатов) будет производиться в этот файл, а не на дисплей терминала. Целевое утверждение tell(X) можно согласовать лишь один раз. Точно так же при выполнении Прологом возврата за целевое утверждение tell не происходит восстановления прежнего текущего выходного потока данных. Наряду со сменой текущего выходного потока данных предикат tell в зависимости от ситуации выполняет также другие операции. В первый раз, когда программа обращается к tell с конкретным именем файла X, Пролог считает, что необходимо завести новый файл с этим именем. Поэтому если X конкретизирована некоторым именем файла и файл с таким именем уже существует, то все литеры, уже содержащиеся в этом файле, удаляются. Напротив, если файла с именем, являющимся значением X, не существует, то файл с таким именем будет создан. В обоих случаях файл считается открытым (для записи в него). Это значит, что каждая последующая запись в этот файл будет добавлять литеры в конец файла до тех пор, пока не появится явное указание, что запись в файл полностью завершена (пока файл не будет закрыт). С данного момента если будет сделана новая попытка записи в этот файл, то Пролог, как и прежде, будет считать, что необходимо писать новую версию этого файла. При попытке выполнить tell(X), когда переменная X не имеет конкретного значения или ее значение не является именем файла, возникает ошибка. Реакция Пролог-системы на возникающие ошибки зависит от конкретной реализации.

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

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

    … tеll(фред), write(X), told,…

    Если текущий выходной поток данных переключается на другой файл без использования told, то прежний файл не будет закрыт и останется доступным для записи в него. Это позволяет делать записи в файл с перерывами, как в следующем примере;


    …tell (X), write(A), tell(user),write(B), tell(X), write(C), told.

    5.4.2. Чтение файлов

    Предикаты, которые предоставляет Пролог для переключения текущего входного потока данных, аналогичны предикатам, обсуждавшимся выше. Целевое утверждение see(X) переключает текущий входной поток данных на файл с указанным именем. Так же как и tell, это целевое утверждение не может быть доказано вновь, и выполненное предикатом изменение входного потока не переделывается при возврате. При первом выполнении see(X) для некоторого файла X этот файл становится открытым (для чтения), при этом чтение начинается с начала файла. Последующая операция чтения продолжает читать данные с того места, где закончила предыдущая операция. И так до тех пор, пока не будет сделано явного закрытия файла. С этого момента новая попытка читать из файла приведет к тому, что файл будет открыт и чтение начнется с начала файла, как и прежде. Текущий входной поток данных может быть определен в результате выполнения seeing(X), и текущий входной поток данных может быть переключен обратно на ввод с терминала в результате выполнения целевого утверждения seen, которое также закрывает файл.

    5.4.3. Ввод программ

    Чтение данных из файлов и запись данных в файлы наиболее полезны тогда, когда объем данных, с которыми работает наша программа и которые мы хотим поместить в базу данных, превосходит тот объем, который мы можем вводить вручную при каждом сеансе работы с ЭВМ. В Прологе файлы используются главным образом для хранения программ. Если текст Пролог-программы содержится в некотором файле, то мы можем прочитать все утверждения, содержащиеся в файле, и поместить их в базу данных, воспользовавшись для этого предикатом consult. Если значением X является имя файла, то цель consult(X) прочитает утверждения (факты и правила) и целевые утверждения из этого файла. Большинство реализаций Пролога имеют специальную форму записи для предиката consult, позволяющую прочитать последовательно один за другим список файлов. Если вопрос к Пролог-системе имеет вид списка атомов, то Пролог-система прочитает каждый файл из этого списка. В качестве примера использования такой формы записи приведем следующий вопрос:


    ?- файл1, отображение, эксперт.


    Этот вопрос обрабатывается таким образом, как если бы Пролог выполнял целевое утверждение consultall(X), где X - это список, заданный в вопросе, а предикат consultall определен следующим образом:


    consultall([]).

    consultall(H|T]):- consult(H), consultall(T).


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

    5.5. Объявление операторов

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

    Синтаксис языка разрешает использование операторов, обладающих следующими тремя свойствами: позицией, приоритетом и ассоциативностью. По занимаемой позиции операторы могут быть инфиксными, постфиксными или префиксными (оператор, имеющий два аргумента, может располагаться между аргументами; оператор с одним аргументом может находиться либо после аргумента, либо перед ним). Приоритет оператора – это некоторое целое число, диапазон изменения которого зависит от конкретной реализации Пролога. Предположим, что оно находится в диапазоне от 1 до 255. Приоритет используется для того, чтобы придать однозначную интерпретацию выражениям в тех случаях, когда синтаксис термов не задан явно с помощью скобок. Ассоциативность необходима для придания однозначной интерпретации выражениям, в которых имеются два оператора с одинаковыми приоритетами. Оператору в языке Пролог соответствует специальный атом, который специфицирует позицию и ассоциативность оператора. Для инфиксных операторов возможны следующие спецификации:

    xfx xfy yfx yfy

    Чтобы понять смысл этих спецификаций, их полезно рассматривать как «образцы» возможного использования операторов. В приведенных образцах буква f представляет оператор, а х и у – аргументы. Таким образом, во всех приведенных выше образцах оператор должен находиться между двумя аргументами, т. е. он является инфиксным оператором. В соответствии с этим соглашением

    fx fy

    есть две спецификации для префиксных операторов (оператор записывается перед его единственным аргументом). Точно так же

    xf yf

    представляют возможные спецификации для постфиксных операторов. Может вызвать недоумение использование двух букв для обозначения аргументов. Использование букв х и у в той или иной позиции позволяет выразить информацию об ассоциативности оператора. В предположении, что выражение не содержит скобок, буква у указывает, что соответствующий ей аргумент может содержать операторы с приоритетом, равным приоритету данного оператора или с более низким приоритетом. Напротив, буква х указывает, что каждый оператор в соответствующем ей аргументе должен иметь строго более низкий приоритет по сравнению с приоритетом данного оператора. Рассмотрим, что это значит для оператора +, объявленного как yfx. Если имеется выражение

    а + b + с

    то для него возможны две следующие интерпретации:

    (а + b) + с      а + (b + с)

    Вторая интерпретация исключается, так как при этом аргумент, стоящий после первого вхождения +, содержит оператор с тем же самым приоритетом (второй оператор +). Это противоречит тому, что в спецификации оператора + после f стоит х.

    Оператор, имеющий спецификацию yfx, является левоассоциативным. Аналогично оператор со спецификацией xfy является правоассоциативным. Если мы знаем необходимую ассоциативность объявляемого инфиксного оператора, то это значит, что однозначно определяется соответствующая оператору спецификация.

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

    not not a

    допустима синтаксически, если оператор not объявлен как fy, и недопустима в случае, когда он объявлен как fx.

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


    ?- ор (Приоритет,Спецификация,Имя).


    Если объявление оператора является допустимым, то эта цель будет достигнута.

    В качестве примера объявления операторов далее приводится полный список базовых операторов, обсуждаемых в данной книге. Конкретные реализации Пролога могут иметь несколько отличный набор «стандартных» операторов; может потребоваться масштабирование указанных приоритетов. Однако взаимный порядок операторов в иерархии приоритетов обычно остается неизменным.


    ?-op(255,xfx,':-').

    ?-op(255,fx,'?-').

    ?-op(254,xfy,';').

    ?-op(253,xfy,',').

    ?-op(250,fx,spy).

    ?-op(250,fx,nospy).

    ?-op(60,fx,not).

    ?-op(51,xfy,'.').

    ?-op(40,xfx,is).

    ?-op(40,xfx,' =..').

    ?-op(40,xfx,=).

    ?-op(40,xfx,\=).

    ?-op(40,xfx,‹).

    ?-op(40,xfx,=‹).

    ?-op(40,xfx,›=).

    ?-op(40,xfx,›).

    ?-op(40,xfx, ==).

    ?-op(40,xfx,\==).

    ?-op(31,yfx,-).

    ?-op(31,yfx,+).

    ?-op(21,yfx,/).

    ?-op(21,yfx,*).

    ?-op(ll,xfx, mod).







     

    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх