Основы программирования на языке Visual Prolog

Внутренняя база данных

Разбить на страницы
Показывать лекцию целиком

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

Внутренние базы данных в языке Пролог состоят из последовательности фактов. Эти факты могут добавляться или удаляться во время исполнения программы, обрабатывается база данных в оперативной памяти компьютера. Поэтому внутреннюю базу данных называют динамической. Предикаты, которые определяются в базе данных, объявляются в разделе (class) facts.

4.1. Факт-переменная

Факты-переменные являются аналогами глобальных переменных в процедурных языках программирования. Они объявляются следующим образом:

class facts
    счетчик : positive := 0.
    список : string* := [].

После знака двоеточия указывается домен факта-переменной, справа от знака := начальное значение. Для изменения значений фактов-переменных используется оператор присваивания :=. Значения можно изменять следующим образом:

счетчик := счетчик + 1,
список := ["Маша" | список]

С помощью факта-переменной подсчитать количество мужчин можно так:

class facts
    counter : positive := 0.
clauses
    run():-
        мужчина(¬_),
            counter := counter + 1,
        fail;
        write(counter),
        _ = readLine().

4.2. Цикл fail

Рассмотрим циклы, которые управляются только с помощью отката. В них будет выполняться перебор следующих фактов:

мужчина("Иван").
мужчина("Павел").
мужчина("Петр").

женщина("Мария").
женщина("Анна").

Цикл fail неоднократно использовался ранее. Он устроен следующим образом:

<действие с возвратом>, fail.

Например, рассмотрим цикл:

run():-
    женщина(X),
        write(X), nl
    fail;
    _ = readLine().

В этом цикле один за другим печатаются имена всех женщин:

Мария
Анна

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

run():-
    женщина(X), 
        nl,
        мужчина(Y),
            write(X, " - ", Y, "\t"), 
    fail;
    _ = readLine().

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

Мария - Иван		Мария - Павел	Мария - Петр
Анна - Иван		Анна - Павел	Анна - Петр

4.3. Цикл foreach

Цикл foreach, также как и предыдущий цикл, не может использоваться без точек возврата. Он имеет вид:

foreach <действие с возвратом> do <процедура> end foreach.

Например:

run():-
    foreach женщина(X) do
        write(X), nl
    end foreach,
    _ = readLine().

Циклы fail и foreach завершаются, когда не остается точек возврата.

В приведенной ниже программе в цикле fail выводятся сведения обо всех произведениях, а в цикле foreach — только об авторах произведений, которые хранятся в четных по номеру записях.

class facts
    book: (integer Номер, symbol Автор, symbol Название).
clauses
    book(1, "Толстой", "Война и мир").
    book(2, "Чехов", "Вишневый сад").
    book(3, "Пушкин", "Евгений Онегин").
    book(4, "Достоевский", "Преступление и наказание").

    run():-
        write("Авторы и их произведения\n\n"),
            book (_, Author, Title),
            write(Author, " - ", Title), nl,
        fail;
        nl,  % вывод авторов книг из четных по номеру записей
        foreach  book(N, Author, _), N mod 2 = 0 do
            write(Author), nl
        end foreach,
        _ = readLine().

Упражнение 1. Организуйте в программе "Книги" (см. листинг 4.1) такой цикл, чтобы он прекращался по достижении книги с номером 3.

4.4. Цикл repeat

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

repeat() <действие> <условие выхода из цикла>.

Определение предиката repeat имеет вид:

class predicates
    repeat: () multi.
clauses
    repeat().
    repeat():- 
        repeat().

Этот предикат ничего не делает, кроме того, что ставит точки возврата, потенциально бесконечное число раз. Условие выхода из цикла должно быть истинным только в момент самого выхода. В остальное время оно вызывает откат. Если в разделе <действие> нет точек возврата, то откат идет к предикату repeat.

Рассмотрим цикл:

печать():-
    С = varM::new(0),			% счетчик
    repeat(),
        C:value := C:value + 1, 		% увеличение счетчика на 1
        nl, 				% переход на новую строку
         foreach женщина(X) do 		% печать имен женщин
            write(X, "\t") 			
        end foreach,
    C:value = 3,			% условие выхода из цикла
    !.
печать().

Объект класса varM хранит изменяемое значение value. В этом цикле выполняется троекратный вывод на печать имен всех женщин:

Мария		Анна
Мария		Анна
Мария		Анна

В следующей программе приводится пример repeat-цикла, который используется для вывода символов из буфера ввода [2, 18]. После запуска программы нужно ввести какой-либо текст (слово) и нажать клавишу Enter. После этого текст будет выведен посимвольно, с небольшой паузой перед выводом каждого символа. Символы преобразуются в верхний регистр.

clauses	% наберите слово, нажмите Enter
run():-
    std::repeat(),
        Char = readChar(),	% считывание символа из буфера ввода
        UpperCaseChar = string::charUpper(Char),
        programControl::sleep(500),		%  1000 = 1 сек
        write(UpperCaseChar),
    Char = '\n', 	% условие выхода из цикла
    !,
    clearInput(), 	% очищает буфер ввода
    _ = readLine();
    succeed().

Предикат readChar считывает символ (char) из буфера ввода. В отличие от предиката readLine он не очищает буфер ввода. Символы домена char заключаются в одинарные кавычки. Предикат charUpper переводит символ в верхний регистр. Предикат sleep прерывает исполнение программы на заданное время, которое указывается в миллисекундах (1000 миллисек = 1 сек).

4.5. Внутренняя база фактов

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

Объявление базы фактов

Внутренняя база данных объявляется следующим образом:

class facts - rel 
    супруг: (string Муж, string Жена).

База данных имеет имя rel. Имена внутренних баз данных принадлежат домену factDB. База данных может не иметь имени. В имплементации одного и того же класса может быть несколько безымянных внутренних баз данных. Но если требуется загружать базу данных из файла или сохранять ее в файле, она должна иметь имя.

Добавление фактов

Предикат asserta добавляет факт в начало базы данных:

asserta(супруг(Муж, Жена)).

Предикаты assert и assertz добавляют факт в конец базы данных:

assert(супруг(Муж, Жена)).

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

Удаление фактов

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

Предикат retract может возвращать значения, хранящиеся в удаляемых фактах. Например, цикл

retract(супруг(Муж, _)), write(Муж), nl, fail.

удаляет все факты для предиката супруг, при этом печатаются имена мужей.

Предикат retractAll удаляет все факты, которые унифицируются с аргументом этого предиката. Этот предикат всегда успешный, значения аргументов он не возвращает:

retractAll(супруг(Муж, _)).

Предикат retractFactDb удаляет все записи из базы данных:

retractFactDb(rel).

Загрузка базы данных из файла и запись в файл

Сохранение в файл фактов базы данных выполняет предикат save:

file::save("family.txt", rel)

Если указанного файла нет, то он будет создан.

Предикат consult загружает факты базы данных из файла, при этом они добавляются к текущему состоянию базы данных:

file::consult("family.txt", rel)
Предикат reconsult заменяет текущее состояние базы данных фактами из файла:
file::reconsult("family.txt", rel)

Следующая программа — это пример работы с внутренней базой данных, изменения в которую вносятся во время исполнения программы посредством диалога с пользователем. Для организации диалога применяется repeat-цикл. Выход из цикла производится только при выборе пользователем команды "Выход". Для обработки ошибок используется конструкция try-catch.

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

class facts - phones
    phone: (string Имя, string НомерТелефона).

class predicates
    load: (string FileName).
    maindialog: ().
    act: (string НомерДействия) determ.
    add: (string Name, string Phone).
    delete: (string Name).
    delete: (string Name, string Phone).
    ask: (string Name [out], string Phone [out]) determ.
    ask: (string Name [out]) determ.
    printDb: ().
clauses
    load(FileName):-
        file::existFile(FileName),
        !,
        try file::consult(FileName, phones)	% обработка ошибок
        catch Error do
            writef("Error %. Unable to load the database from "
                "the file %\n", Error, FileName)
        end try;
        succeed().

    maindialog():-
        std::repeat(),
            write("\nВыберите действие:\n",
                "\t1. Просмотр базы данных\n",
                "\t2. Добавить запись\n",
                "\t3. Удалить запись\n",
                "\t4. Выход\n"),
            X = readLine(),
        act(X),
        !.
    maindialog().

    act("1"):- !,
        write("\nВсе записи\n"),
        printDb(),
        fail.
    act("2"):- !,
        write("\nДобавление записи"),
        ask(Name, Phone),
        add(Name, Phone),
        fail.
    act("3"):- !,
        write("\nУдаление записей. Выберите действие:\n",
            "\t3.1 Удаление одной записи\n",
            "\t3.2 Удаление всех записей\n"),
        X = readLine(),
        act(X).
    act("3.1"):- !,
        printDb(),
        ask(Name, Phone),
        delete(Name, Phone),
        fail.
    act("3.2"):- !,
        ask(Name),
        delete(Name),
        fail.
    act("4"):- !, write("\nВыход.\n").
    act(_):- 
        X = readLine(), 
        act(X).

    printDb():-
        phone(Name, Phone),
            writef("  %-20%\n", Name, Phone),
        fail;
        succeed().
 
    ask(Name, Phone):-
        ask(Name),
        write("Введите номер: "),
        Phone = string::trim(readLine()).

    ask(Name):-
        write("\nВведите имя: "),
        Name = string::trim(readLine()),
        Name <> "".

    add(Name, Phone):-
        phone(Name, Phone),
        !,
        writef("Запись % - % уже есть в базе данных\n", 
            Name, Phone);
        assert(phone(Name, Phone)),
        writef("Запись '% - %' добавлена\n", Name, Phone).

    delete(Name):-
        not(phone(Name, _)),
        !,
        writef("Записей для '%' нет\n", Name);
        retractall(phone(Name, _)),
        writef("Записи для '%' удалены\n", Name).

    delete(Name, Phone):-
        retract(phone(Name, Phone)),
        !,
        writef("Запись '% - %' удалена\n", Name, Phone);
        writef("Записи '% - %' нет в базе данных\n", Name, Phone).

    run():-
        setConsoleTitle("Телефоны"),
        FileName = "phones.txt",
        load(FileName),
        maindialog(),
        file::save(FileName, phones),
        _ = readLine().

Файл "phones.txt" создается в директории Exe проекта. Предикат setConsoleTitle устанавливает заголовок окна консоли, предикат writef выполняет форматированный вывод. Предикат trim удаляет пробельные символы в начале и в конце строки. Предикат existFile проверяет, существует ли указанный файл.

Упражнение 2. Добавьте в программу про телефоны (листинг 4.3) поиск номеров телефонов по имени и поиск имени по номеру телефона.

4.6. Работа с файлами

Рассмотрим пример работы с файлами в языке Visual Prolog. В приведенной ниже программе на вход подается имя текстового файла. Определяется способ его кодировки, Unicode или нет, а затем создаются два ANSI-файла. В один из них записываются четные по номеру строки из исходного файла, а в другой — нечетные.

class facts
    n : positive := 0.

class predicates
    type: (string FileName, stream::mode Mode) procedure (i,o).
    rewrite: (string, stream::mode, string File1, string File2)
        determ (i,o,i,i).
clauses
    type(FileName, stream::unicode()):-
        file::isUnicode(FileName),
        !.
    type(_, stream::ansi(ansi())).

    rewrite(FileName, Mode, FileName1, FileName2):-
        file::existFile(FileName),
        type(FileName, Mode),
        Input = inputStream_file::openFile(FileName, Mode),
        F1 = outputStream_file::create8(FileName1),
        F2 = outputStream_file::create8(FileName2),
        std::repeat(),
            Str = Input:readLine(),
            n := 1 - n,
            if n = 1 then
                F1:write(Str), F1:nl
            else
                F2:write(Str), F2:nl
            end if,
        Input:endOfStream(),
        !,
        Input:close(),
        F1:close(),
        F2:close().

    run():-
        FileName = "mytest.txt", File1 = "test1.txt", File2 = "test2.txt",
        rewrite(FileName, Mode, File1, File2),
        writef("FileName %\nMode: %\nЧисло строк = %\n",
            FileName, Mode, n),
        writef("Нечетные строки в файле %, а четные в файле %\n",
            File1, File2),
        write(file::readString(FileName, _)), % счит. файл в string
        fail;
        _ = readLine().

Предикат isUnicode проверяет, используется ли для файла кодировка символов Юникод. Конструктор openFile открывает файл для чтения, предикат close закрывает файл. Предикат create8 создает ANSI-файл для записи. Предикат endOfStream проверяет, достигнут ли конец файла. Предикат readString считывает файл в строку.

4.7. Атрибуты текста в консоли. Очередь событий

В системе Visual Prolog имеются средства, которые позволяют использовать в консольных приложениях цвет, события мыши, события нажатия клавиши и др. Программа, приведенная ниже, создает в окне консоли цветную полосу, состоящую из "квадратов" (шириной в два пробельных символа), цвет которых генерируется случайным образом в процессе repeat-цикла. Задействовано 15 цветов, черный цвет не используется. Перед появлением каждого "квадрата" делается небольшая пауза. Цикл прекращается после щелчка мыши, когда курсор находится в произвольном месте окна консоли (рис. 4.1).

(рис 4.1) Разноцветная полоса; цвета генерируются сучайным образом

В комментарии указано, как остановить цикл посредством нажатия определенной клавиши.

    open core, console, console_native

class predicates
    getAttribute: (unsigned16 ЦветФона, unsigned16 ЦветТекста) 
        -> unsigned16.
clauses
    getAttribute(BgColor, TextColor) =
        bit::bitOr(bit::bitLeft(BgColor, 4), TextColor).

class predicates
    stop: () determ.
    process: ().
clauses
    stop():-
        L = getEventQueue(),
    %    key(_, _, _, _, 'k', _) = list::getMember_nd(L),
        mouse( _, _, 1, _, 0) = list::getMember_nd(L),
        !.

    process():-
        std::repeat(),
            N = 1 + math::random(15),
            setTextAttribute(bit::bitLeft(N, 4)),    % или (N * 16)
            write("  "),				% два пробельных символа
            programControl::sleep(100),
        stop(),
        !.
    process().

    run():-
        setLocation(coord(20, 10)),
        setTextAttribute(getAttribute(3, 15)),
        write("Движение прекратится по щелчку мыши"),
        programControl::sleep(3000),	% 3 сек
        clearOutput(),			% очищает буфер вывода
        setLocation(coord(0, 1)),
        process(),
        _ = readLine().

Предикат getEventQueue возвращает для окна консоли список текущих событий. Предикат getMember_nd недетерминированно возвращает произвольный элемент списка. Предикат setTextAttribute устанавливает в консоли атрибуты текста. Предикат setLocation помещает курсор в точку с заданными координатами. Началом координат в окне консоли является его левый верхний угол, ось абсцисс направлена вправо, а ось ординат — вниз. Единицей измерения по оси абсцисс является ширина одного символа (все они имеют одинаковую ширину), а по оси ординат — высота строки. Предикат random генерирует целое неотрицательное случайное число. Если его аргумент равен R, то генерируется число в пределах от 0 до R – 1 включительно.

Упражнения

  • Решите ребус: $$УФ \circ ФУ = УШУ$$. В этой записи разным буквам соответствуют разные цифры, причем первая цифра в записи чисел отлична от нуля.
  • Создайте базу данных, в которой для каждого сотрудника хранятся фамилия, должность и оклад. Определите средний размер оклада у программистов.
  • Создайте базу данных "Мое расписание", в которой хранятся факты для описания отношения расписание вида:

    расписание(время(пт, 1), 
    предмет("Интеллектуальные системы", лекция), 
    преп("Аншаков О.М.", профессор), место(416, 2)).
    

    Факты должны содержать следующие сведения: день недели и номер пары; предмет — название и вид занятия (лекционное и т. д.); фамилия и инициалы преподавателя, его должность; аудитория и корпус.

    Найдите ответы на следующие вопросы.

  • Кто ведет "Английский язык"?
  • Когда и где проходят занятия по курсу "Экспертные системы"?
  • В каких аудиториях проводятся занятия по вторникам и средам?
  • В какие дни недели ровно три пары?
  • По какие предметам лабораторные занятия ведет только лектор.
  • Найдите количество строк в файле.
  • Запишите в новый файл поочередно строки то из одного файла, то из другого.
  • Реализуйте вывод цветной полосы "змейкой".
  • Напишите программу, которая генерирует случайным образом 15 попарно различных точек плоскости с целыми координатами в пределах от 0 до 79 по оси абсцисс и от 0 до 22 по оси ординат. Найдите все тройки точек, которые образуют треугольники максимальной площади. Отобразите точки в окне консоли. Вершины найденных треугольников выделите цветом.
  • Напишите программу для тестирования учащихся по какому-либо предмету. Тест состоит из n вопросов. Приводятся варианты ответов, среди которых один правильный. Тестируемый должен указать номер правильного ответа. За каждый правильный ответ ему начисляется m баллов. Результатом теста является сумма полученных баллов.
  • Страницы:

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

    Внутренние базы данных в языке Пролог состоят из последовательности фактов. Эти факты могут добавляться или удаляться во время исполнения программы, обрабатывается база данных в оперативной памяти компьютера. Поэтому внутреннюю базу данных называют динамической. Предикаты, которые определяются в базе данных, объявляются в разделе (class) facts.

    4.1. Факт-переменная

    Факты-переменные являются аналогами глобальных переменных в процедурных языках программирования. Они объявляются следующим образом:

    class facts
        счетчик : positive := 0.
        список : string* := [].
    

    После знака двоеточия указывается домен факта-переменной, справа от знака := начальное значение. Для изменения значений фактов-переменных используется оператор присваивания :=. Значения можно изменять следующим образом:

    счетчик := счетчик + 1,
    список := ["Маша" | список]

    С помощью факта-переменной подсчитать количество мужчин можно так:

    class facts
        counter : positive := 0.
    clauses
        run():-
            мужчина(¬_),
                counter := counter + 1,
            fail;
            write(counter),
            _ = readLine().
    

    4.2. Цикл fail

    Рассмотрим циклы, которые управляются только с помощью отката. В них будет выполняться перебор следующих фактов:

    мужчина("Иван").
    мужчина("Павел").
    мужчина("Петр").
    
    женщина("Мария").
    женщина("Анна").
    

    Цикл fail неоднократно использовался ранее. Он устроен следующим образом:

    <действие с возвратом>, fail.
    

    Например, рассмотрим цикл:

    run():-
        женщина(X),
            write(X), nl
        fail;
        _ = readLine().
    

    В этом цикле один за другим печатаются имена всех женщин:

    Мария
    Анна
    

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

    run():-
        женщина(X), 
            nl,
            мужчина(Y),
                write(X, " - ", Y, "\t"), 
        fail;
        _ = readLine().
    

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

    Мария - Иван		Мария - Павел	Мария - Петр
    Анна - Иван		Анна - Павел	Анна - Петр
    

    4.3. Цикл foreach

    Цикл foreach, также как и предыдущий цикл, не может использоваться без точек возврата. Он имеет вид:

    foreach <действие с возвратом> do <процедура> end foreach.
    

    Например:

    run():-
        foreach женщина(X) do
            write(X), nl
        end foreach,
        _ = readLine().
    

    Циклы fail и foreach завершаются, когда не остается точек возврата.

    В приведенной ниже программе в цикле fail выводятся сведения обо всех произведениях, а в цикле foreach — только об авторах произведений, которые хранятся в четных по номеру записях.

    class facts
        book: (integer Номер, symbol Автор, symbol Название).
    clauses
        book(1, "Толстой", "Война и мир").
        book(2, "Чехов", "Вишневый сад").
        book(3, "Пушкин", "Евгений Онегин").
        book(4, "Достоевский", "Преступление и наказание").
    
        run():-
            write("Авторы и их произведения\n\n"),
                book (_, Author, Title),
                write(Author, " - ", Title), nl,
            fail;
            nl,  % вывод авторов книг из четных по номеру записей
            foreach  book(N, Author, _), N mod 2 = 0 do
                write(Author), nl
            end foreach,
            _ = readLine().
    

    Упражнение 1. Организуйте в программе "Книги" (см. листинг 4.1) такой цикл, чтобы он прекращался по достижении книги с номером 3.

    4.4. Цикл repeat

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

    repeat() <действие> <условие выхода из цикла>.
    

    Определение предиката repeat имеет вид:

    class predicates
        repeat: () multi.
    clauses
        repeat().
        repeat():- 
            repeat().
    

    Этот предикат ничего не делает, кроме того, что ставит точки возврата, потенциально бесконечное число раз. Условие выхода из цикла должно быть истинным только в момент самого выхода. В остальное время оно вызывает откат. Если в разделе <действие> нет точек возврата, то откат идет к предикату repeat.

    Рассмотрим цикл:

    печать():-
        С = varM::new(0),			% счетчик
        repeat(),
            C:value := C:value + 1, 		% увеличение счетчика на 1
            nl, 				% переход на новую строку
             foreach женщина(X) do 		% печать имен женщин
                write(X, "\t") 			
            end foreach,
        C:value = 3,			% условие выхода из цикла
        !.
    печать().
    

    Объект класса varM хранит изменяемое значение value. В этом цикле выполняется троекратный вывод на печать имен всех женщин:

    Мария		Анна
    Мария		Анна
    Мария		Анна
    

    В следующей программе приводится пример repeat-цикла, который используется для вывода символов из буфера ввода [2, 18]. После запуска программы нужно ввести какой-либо текст (слово) и нажать клавишу Enter. После этого текст будет выведен посимвольно, с небольшой паузой перед выводом каждого символа. Символы преобразуются в верхний регистр.

    clauses	% наберите слово, нажмите Enter
    run():-
        std::repeat(),
            Char = readChar(),	% считывание символа из буфера ввода
            UpperCaseChar = string::charUpper(Char),
            programControl::sleep(500),		%  1000 = 1 сек
            write(UpperCaseChar),
        Char = '\n', 	% условие выхода из цикла
        !,
        clearInput(), 	% очищает буфер ввода
        _ = readLine();
        succeed().
    

    Предикат readChar считывает символ (char) из буфера ввода. В отличие от предиката readLine он не очищает буфер ввода. Символы домена char заключаются в одинарные кавычки. Предикат charUpper переводит символ в верхний регистр. Предикат sleep прерывает исполнение программы на заданное время, которое указывается в миллисекундах (1000 миллисек = 1 сек).

    4.5. Внутренняя база фактов

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

    Объявление базы фактов

    Внутренняя база данных объявляется следующим образом:

    class facts - rel 
        супруг: (string Муж, string Жена).
    

    База данных имеет имя rel. Имена внутренних баз данных принадлежат домену factDB. База данных может не иметь имени. В имплементации одного и того же класса может быть несколько безымянных внутренних баз данных. Но если требуется загружать базу данных из файла или сохранять ее в файле, она должна иметь имя.

    Добавление фактов

    Предикат asserta добавляет факт в начало базы данных:

    asserta(супруг(Муж, Жена)).
    

    Предикаты assert и assertz добавляют факт в конец базы данных:

    assert(супруг(Муж, Жена)).
    

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

    Удаление фактов

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

    Предикат retract может возвращать значения, хранящиеся в удаляемых фактах. Например, цикл

    retract(супруг(Муж, _)), write(Муж), nl, fail.
    

    удаляет все факты для предиката супруг, при этом печатаются имена мужей.

    Предикат retractAll удаляет все факты, которые унифицируются с аргументом этого предиката. Этот предикат всегда успешный, значения аргументов он не возвращает:

    retractAll(супруг(Муж, _)).
    

    Предикат retractFactDb удаляет все записи из базы данных:

    retractFactDb(rel).
    

    Загрузка базы данных из файла и запись в файл

    Сохранение в файл фактов базы данных выполняет предикат save:

    file::save("family.txt", rel)
    

    Если указанного файла нет, то он будет создан.

    Предикат consult загружает факты базы данных из файла, при этом они добавляются к текущему состоянию базы данных:

    file::consult("family.txt", rel)
    Предикат reconsult заменяет текущее состояние базы данных фактами из файла:
    file::reconsult("family.txt", rel)
    

    Следующая программа — это пример работы с внутренней базой данных, изменения в которую вносятся во время исполнения программы посредством диалога с пользователем. Для организации диалога применяется repeat-цикл. Выход из цикла производится только при выборе пользователем команды "Выход". Для обработки ошибок используется конструкция try-catch.

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

    class facts - phones
        phone: (string Имя, string НомерТелефона).
    
    class predicates
        load: (string FileName).
        maindialog: ().
        act: (string НомерДействия) determ.
        add: (string Name, string Phone).
        delete: (string Name).
        delete: (string Name, string Phone).
        ask: (string Name [out], string Phone [out]) determ.
        ask: (string Name [out]) determ.
        printDb: ().
    clauses
        load(FileName):-
            file::existFile(FileName),
            !,
            try file::consult(FileName, phones)	% обработка ошибок
            catch Error do
                writef("Error %. Unable to load the database from "
                    "the file %\n", Error, FileName)
            end try;
            succeed().
    
        maindialog():-
            std::repeat(),
                write("\nВыберите действие:\n",
                    "\t1. Просмотр базы данных\n",
                    "\t2. Добавить запись\n",
                    "\t3. Удалить запись\n",
                    "\t4. Выход\n"),
                X = readLine(),
            act(X),
            !.
        maindialog().
    
        act("1"):- !,
            write("\nВсе записи\n"),
            printDb(),
            fail.
        act("2"):- !,
            write("\nДобавление записи"),
            ask(Name, Phone),
            add(Name, Phone),
            fail.
        act("3"):- !,
            write("\nУдаление записей. Выберите действие:\n",
                "\t3.1 Удаление одной записи\n",
                "\t3.2 Удаление всех записей\n"),
            X = readLine(),
            act(X).
        act("3.1"):- !,
            printDb(),
            ask(Name, Phone),
            delete(Name, Phone),
            fail.
        act("3.2"):- !,
            ask(Name),
            delete(Name),
            fail.
        act("4"):- !, write("\nВыход.\n").
        act(_):- 
            X = readLine(), 
            act(X).
    
        printDb():-
            phone(Name, Phone),
                writef("  %-20%\n", Name, Phone),
            fail;
            succeed().
     
        ask(Name, Phone):-
            ask(Name),
            write("Введите номер: "),
            Phone = string::trim(readLine()).
    
        ask(Name):-
            write("\nВведите имя: "),
            Name = string::trim(readLine()),
            Name <> "".
    
        add(Name, Phone):-
            phone(Name, Phone),
            !,
            writef("Запись % - % уже есть в базе данных\n", 
                Name, Phone);
            assert(phone(Name, Phone)),
            writef("Запись '% - %' добавлена\n", Name, Phone).
    
        delete(Name):-
            not(phone(Name, _)),
            !,
            writef("Записей для '%' нет\n", Name);
            retractall(phone(Name, _)),
            writef("Записи для '%' удалены\n", Name).
    
        delete(Name, Phone):-
            retract(phone(Name, Phone)),
            !,
            writef("Запись '% - %' удалена\n", Name, Phone);
            writef("Записи '% - %' нет в базе данных\n", Name, Phone).
    
        run():-
            setConsoleTitle("Телефоны"),
            FileName = "phones.txt",
            load(FileName),
            maindialog(),
            file::save(FileName, phones),
            _ = readLine().
    

    Файл "phones.txt" создается в директории Exe проекта. Предикат setConsoleTitle устанавливает заголовок окна консоли, предикат writef выполняет форматированный вывод. Предикат trim удаляет пробельные символы в начале и в конце строки. Предикат existFile проверяет, существует ли указанный файл.

    Упражнение 2. Добавьте в программу про телефоны (листинг 4.3) поиск номеров телефонов по имени и поиск имени по номеру телефона.

    4.6. Работа с файлами

    Рассмотрим пример работы с файлами в языке Visual Prolog. В приведенной ниже программе на вход подается имя текстового файла. Определяется способ его кодировки, Unicode или нет, а затем создаются два ANSI-файла. В один из них записываются четные по номеру строки из исходного файла, а в другой — нечетные.

    class facts
        n : positive := 0.
    
    class predicates
        type: (string FileName, stream::mode Mode) procedure (i,o).
        rewrite: (string, stream::mode, string File1, string File2)
            determ (i,o,i,i).
    clauses
        type(FileName, stream::unicode()):-
            file::isUnicode(FileName),
            !.
        type(_, stream::ansi(ansi())).
    
        rewrite(FileName, Mode, FileName1, FileName2):-
            file::existFile(FileName),
            type(FileName, Mode),
            Input = inputStream_file::openFile(FileName, Mode),
            F1 = outputStream_file::create8(FileName1),
            F2 = outputStream_file::create8(FileName2),
            std::repeat(),
                Str = Input:readLine(),
                n := 1 - n,
                if n = 1 then
                    F1:write(Str), F1:nl
                else
                    F2:write(Str), F2:nl
                end if,
            Input:endOfStream(),
            !,
            Input:close(),
            F1:close(),
            F2:close().
    
        run():-
            FileName = "mytest.txt", File1 = "test1.txt", File2 = "test2.txt",
            rewrite(FileName, Mode, File1, File2),
            writef("FileName %\nMode: %\nЧисло строк = %\n",
                FileName, Mode, n),
            writef("Нечетные строки в файле %, а четные в файле %\n",
                File1, File2),
            write(file::readString(FileName, _)), % счит. файл в string
            fail;
            _ = readLine().
    

    Предикат isUnicode проверяет, используется ли для файла кодировка символов Юникод. Конструктор openFile открывает файл для чтения, предикат close закрывает файл. Предикат create8 создает ANSI-файл для записи. Предикат endOfStream проверяет, достигнут ли конец файла. Предикат readString считывает файл в строку.

    4.7. Атрибуты текста в консоли. Очередь событий

    В системе Visual Prolog имеются средства, которые позволяют использовать в консольных приложениях цвет, события мыши, события нажатия клавиши и др. Программа, приведенная ниже, создает в окне консоли цветную полосу, состоящую из "квадратов" (шириной в два пробельных символа), цвет которых генерируется случайным образом в процессе repeat-цикла. Задействовано 15 цветов, черный цвет не используется. Перед появлением каждого "квадрата" делается небольшая пауза. Цикл прекращается после щелчка мыши, когда курсор находится в произвольном месте окна консоли (рис. 4.1).

    (рис 4.1) Разноцветная полоса; цвета генерируются сучайным образом

    В комментарии указано, как остановить цикл посредством нажатия определенной клавиши.

        open core, console, console_native
    
    class predicates
        getAttribute: (unsigned16 ЦветФона, unsigned16 ЦветТекста) 
            -> unsigned16.
    clauses
        getAttribute(BgColor, TextColor) =
            bit::bitOr(bit::bitLeft(BgColor, 4), TextColor).
    
    class predicates
        stop: () determ.
        process: ().
    clauses
        stop():-
            L = getEventQueue(),
        %    key(_, _, _, _, 'k', _) = list::getMember_nd(L),
            mouse( _, _, 1, _, 0) = list::getMember_nd(L),
            !.
    
        process():-
            std::repeat(),
                N = 1 + math::random(15),
                setTextAttribute(bit::bitLeft(N, 4)),    % или (N * 16)
                write("  "),				% два пробельных символа
                programControl::sleep(100),
            stop(),
            !.
        process().
    
        run():-
            setLocation(coord(20, 10)),
            setTextAttribute(getAttribute(3, 15)),
            write("Движение прекратится по щелчку мыши"),
            programControl::sleep(3000),	% 3 сек
            clearOutput(),			% очищает буфер вывода
            setLocation(coord(0, 1)),
            process(),
            _ = readLine().
    

    Предикат getEventQueue возвращает для окна консоли список текущих событий. Предикат getMember_nd недетерминированно возвращает произвольный элемент списка. Предикат setTextAttribute устанавливает в консоли атрибуты текста. Предикат setLocation помещает курсор в точку с заданными координатами. Началом координат в окне консоли является его левый верхний угол, ось абсцисс направлена вправо, а ось ординат — вниз. Единицей измерения по оси абсцисс является ширина одного символа (все они имеют одинаковую ширину), а по оси ординат — высота строки. Предикат random генерирует целое неотрицательное случайное число. Если его аргумент равен R, то генерируется число в пределах от 0 до R – 1 включительно.

    Упражнения

  • Решите ребус: $$УФ \circ ФУ = УШУ$$. В этой записи разным буквам соответствуют разные цифры, причем первая цифра в записи чисел отлична от нуля.
  • Создайте базу данных, в которой для каждого сотрудника хранятся фамилия, должность и оклад. Определите средний размер оклада у программистов.
  • Создайте базу данных "Мое расписание", в которой хранятся факты для описания отношения расписание вида:

    расписание(время(пт, 1), 
    предмет("Интеллектуальные системы", лекция), 
    преп("Аншаков О.М.", профессор), место(416, 2)).
    

    Факты должны содержать следующие сведения: день недели и номер пары; предмет — название и вид занятия (лекционное и т. д.); фамилия и инициалы преподавателя, его должность; аудитория и корпус.

    Найдите ответы на следующие вопросы.

  • Кто ведет "Английский язык"?
  • Когда и где проходят занятия по курсу "Экспертные системы"?
  • В каких аудиториях проводятся занятия по вторникам и средам?
  • В какие дни недели ровно три пары?
  • По какие предметам лабораторные занятия ведет только лектор.
  • Найдите количество строк в файле.
  • Запишите в новый файл поочередно строки то из одного файла, то из другого.
  • Реализуйте вывод цветной полосы "змейкой".
  • Напишите программу, которая генерирует случайным образом 15 попарно различных точек плоскости с целыми координатами в пределах от 0 до 79 по оси абсцисс и от 0 до 22 по оси ординат. Найдите все тройки точек, которые образуют треугольники максимальной площади. Отобразите точки в окне консоли. Вершины найденных треугольников выделите цветом.
  • Напишите программу для тестирования учащихся по какому-либо предмету. Тест состоит из n вопросов. Приводятся варианты ответов, среди которых один правильный. Тестируемый должен указать номер правильного ответа. За каждый правильный ответ ему начисляется m баллов. Результатом теста является сумма полученных баллов.
  • Вернуться к учебному плану