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

Язык запросов

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

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

В данной главе создается консольный проект в объектном стиле.

Язык Visual Prolog — объектно-ориентированный. Классы и интерфейсы создаются с помощью диалогового окна Create Project Item. Классы могут иметь интерфейс, а могут не иметь интерфейса. Объекты порождают те, и только те классы, которые имеют интерфейс.

Декларация класса помещается в файл с расширением cl, интерфейс — в файл с расширением i, имплементация — в файл с расширением pro.

Рассмотрим пример интерфейса, декларации класса с этим интерфейсом и его имплементации.

interface person      % интерфейс
    open core

    properties
        id : unsigned.
        name : string.
        age : positive.
end interface person
%---------------------------------------------------------------------------
class person : person      % декларация класса
    open core

    constructors        % объявление конструктора
        new: (unsigned Id).

    predicates
        count: () -> positive.
end class student
%---------------------------------------------------------------------------
implement person        % имплементация класса
    open core

    facts
        id : unsigned.
        name : string := "".
        age : positive := 18.
    
    class facts
        counter : positive := 0.

    clauses
        new(Id):-
            id := Id,
            counter := counter + 1.

        count() = counter.
end implement person
%---------------------------------------------------------------------------
goal            % цель программы
    mainExe::run(main::run),
    P1 = person::new(1),
    P1:name := "Вася",
    P1:age := 18,
    P2 = person::new(2),
    P2:name := "Маша",
    P2:age := 19,
    stdio::writef("%. % - %\n%. % - %\nКоличество = %",
        P1:id, P1:name, P1:age, P2:id, P2:name, P2:age,
        person::count()).

В разделе properties объявляются свойства. Обычно они определяются в имплементации класса с помощью фактов-переменных (см. выше). Определение свойства может выглядеть и следующим образом:

facts
    n: positive.
clauses
    age() = n:-
        stdio::writef("Считано значение свойства age = %\n", n).
    age(V):-
        n := V,
        stdio::writef("Свойству age присвоено значение %\n", n).

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

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

Подробнее об объектно-ориентированном программировании в языке Visual Prolog см. [17].

12.1. База данных

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

Для реализации доступа к сведениям, хранящимся в базе данных, создается класс dbrel. Объект базы данных создается по имени файла, в котором хранятся факты базы данных.

В базе данных хранятся сведения об именах и фамилиях людей, их поле, отце, матери и супругах. Каждый человек имеет уникальный идентификатор. При записи сведений о новом человеке ему автоматически присваивается идентификатор. Для хранения записей используется два отношения — person/6 и spouse/2.

Ниже приведен интерфейс общения с базой данных (файл dbrel.i).

constants
    male = "м".
    female = "ж".

predicates
    load: ().
    save: ().
    person_nd: (unsigned, string, string, string, unsigned, unsigned)
        nondeterm (o,o,o,o,o,o) (o,i,o,o,o,o) (o,i,i,o,o,o) (o,o,o,i,o,o)
           (o,o,o,o,i,o) (i,o,o,o,o,o) (i,o,o,o,i,o) (o,o,o,o,o,i) (i,o,o,o,o,i).
    spouse_nd: (unsigned, unsigned) nondeterm (o,o) (i,o) (o,i).
    getSex: (unsigned Id) -> string determ.
    getNewId: () -> unsigned.
    setPerson: (unsigned, string, string, string, unsigned, unsigned).
    setSpouse: (unsigned, unsigned).
    delPerson: (unsigned).
    delSpouse: (unsigned, unsigned).

Конструктор объявляется так, как показано ниже (файл dbrel.cl).

constructors
    new: (string FileName).

Имплементация приведена в листинге 12.4 (файл dbrel.pro).

facts
    filename : string.
    maxId : unsigned := 0.
clauses
    new(FileName):-
        filename := FileName.

facts - rel
    person: (unsigned, string, string, string, unsigned, unsigned).
    spouse: (unsigned IdHusband, unsigned IdWife).

clauses
    load():-
        try file::consult(filename, rel)
        catch Error do
            writef("Error %. Unable to load the database from "
                "the file %\n", Error, filename)
        end try,
        setMaxId().
        
    save():-
        file::save(filename, rel),
        write("\nИзменения сохранены в базы данных.").

    person_nd(Id, Name, Surname, Sex, IdF, IdM):-
        person(Id, Name, Surname, Sex, IdF, IdM).

    spouse_nd(IdH, IdW):-
        spouse(IdH, IdW).

    getSex(Id) = Sex:-
        person(Id, _, _, Sex, _, _),
        !.

    getNewId() = maxId:-
        maxId := maxId + 1.

    setPerson(Id, Name, Surname, Sex, IdF, IdM):-
        assert(person(Id, Name, Surname, Sex, IdF, IdM)),
        writef("Запись '% - % - % - % - % - %' добавлена\n",
            Id, Name, Surname, Sex, IdF, IdM).

    setSpouse(IdH, IdW):-
        assert(spouse(IdH, IdW)),
        writef("Запись '% - %' добавлена\n", IdH, IdW).

    delPerson(Id):-
        retractAll(person(Id, _, _, _, _, _)),
        writef("Записи о % удалены\n", Id).

    delSpouse(IdH, IdW):-
        retractAll(spouse(IdH, IdW)),
        writef("Запись '% - %' удалена\n", IdH, IdW).

predicates
    setMaxId: ().
clauses
    setMaxId():-
        L = [Id || person(Id, _, _, _, _, _)],
        L <> [],
        !,
        maxId := list::maximum(L).
    setMaxId().

12.2. Определение основных отношений

В настоящем параграфе определяются основные родственные отношения.

Создается класс relation для описания родственных отношений. Аргументами всех отношений являются идентификаторы людей. Ниже приведен интерфейс relation (файл relation.i).

properties
    db : dbrel.

predicates
    parent: (unsigned, unsigned) nondeterm (o,i) (o,o) (i,o) (i,i).
    father: (unsigned, unsigned) nondeterm (o,i) (o,o) (i,o) (i,i).
    mother: (unsigned, unsigned) nondeterm (o,i) (o,o) (i,o) (i,i).
    husband: (unsigned, unsigned) nondeterm (o,i) (i,o) (o,o).
    son: (unsigned, unsigned) nondeterm (o,i) (o,o).
    daughter: (unsigned, unsigned) nondeterm (o,i) (o,o).
    sister: (unsigned, unsigned) nondeterm (o,i) (o,o).
    brother: (unsigned, unsigned) nondeterm (o,i) (o,o).
    ancestor: (unsigned, unsigned) nondeterm (o,i) (i,o) (o,o) (i,i).

Объявление конструктора приведено в листинге 12.6 (файл relation.cl).

constructors
    new: (dbrel).

Ниже приведена имплементация (файл relation.pro).

facts
    db: dbrel.
clauses
    new(Db):-
        db := Db.

predicates
    male: (unsigned [out]) nondeterm.
    female: (unsigned [out]) nondeterm.
clauses
    male(Id):-
        db:person_nd(Id, _, _, dbrel::male, _, _).

    female(Id):-
        db:person_nd(Id, _, _, dbrel::female, _, _).

clauses
    father(IdF, IdCh):-
        db:person_nd(IdCh, _, _, _, IdF, _),
        IdF > 0.

    mother(IdM, IdCh):-
        db:person_nd(IdCh, _, _, _, _, IdM),
        IdM > 0.

    parent(IdP, IdCh):-
        father(IdP, IdCh);
        mother(IdP, IdCh).

    husband(IdH, IdW):-
        db:spouse_nd(IdH, IdW).

     son(IdS, IdP):-
        male(IdS),
        parent(IdP, IdS).

    daughter(IdD, IdP):-
        female(IdD),
        parent(IdP, IdD).

    ancestor(IdA, IdD):-
        parent(IdA, IdD).
    ancestor(IdA, IdD):-
        parent(IdA, X),
        ancestor(X, IdD).

predicates
    sister_nd: (unsigned, unsigned) nondeterm (o,i) (o,o).
    brother_nd: (unsigned, unsigned) nondeterm (o,i) (o,o).
    get_nd: (A*) -> A nondeterm.
clauses
    sister_nd(IdS, IdSib):-
        female(IdS),
        parent(P, IdS),
        parent(P, IdSib),
        IdS <> IdSib.

    brother_nd(IdB, IdSib):-
        male(IdB),
        parent(P, IdB),
        parent(P, IdSib),
        IdB <> IdSib.

    brother(IdB, Id):-
        free(Id),
        tuple(IdB, Id) = get_nd([tuple(I, I1) || brother_nd(I, I1)]).
    brother(IdB, Id):-
        bound(Id),
        IdB = get_nd([I || brother_nd(I, Id)]).

    sister(IdS, Id):-
        free(Id),
        tuple(IdS, Id) = get_nd([tuple(I, I1) || sister_nd(I, I1)]).
    sister(IdS, Id):-
        bound(Id),
        IdS = get_nd([I || sister_nd(I, Id)]).

    get_nd(L) = list::getMember_nd(list::removeDuplicates(L)).

Предикат free/1 истинен, если его аргумент является свободной переменной, а предикат bound/1, соответственно, если конкретизированной.

12.3. Диалог с пользователем

В настоящем параграфе рассматривается реализация общения пользователя с компьютером.

Создается модуль mainDialog для реализации диалога с базой данных. Во время диалога пользователь может просмотреть базу данных, отредактировать базу данных (добавить или удалить факты) и просмотреть основные родственные отношения. Например, отношение "родитель – ребенок" имеет вид:

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

Кроме этого, пользователь может посмотреть на дерево предков и на дерево потомков человека, а также задать запрос к базе данных на языке, приближенном к естественному языку. Запросы должны удовлетворять определенной грамматике. Для организации диалога используется repeat-цикл. Выход из цикла выполняется по желанию пользователя о выходе из программы. Перед выходом программа спрашивает, сохранить ли изменения в базе данных. Если пользователь не дает положительный ответ, то изменения не сохраняются.

В декларацию класса mainDialog (листинг 12.8) следует поместить объявление предиката, который вызывается в разделе goal (файл mainDialog.cl).

predicates
    run: ().

Раздел goal проекта должен быть изменен следующим образом (файл main.pro):

goal
    mainExe::run(main::run),
    mainDialog::run().

Имплементация класса mainDialog приведена ниже (файл mainDialog.pro). Указатели на объекты классов dbrel, relation, printData и readData (они описываются ниже) хранятся в фактах-переменных. При инициализации им присваивается "значение" erroneous, что по-существу, означает, что значение им присвоено не было. Это "значение" можно присваивать динамически, кроме этого, имеется предикат проверки isErroneous(имя_факта), что часто оказывается полезным в GUI-приложениях.

open core, console

class facts
    db : dbrel := erroneous.
    rel : relation := erroneous.
    pd : printData := erroneous.
    rd : readData := erroneous.

class predicates
    maindialog: ().
clauses
    maindialog():-
        std::repeat(),
            write("\nВыберите действие:\n"
                "\t1. Просмотр базы данных\n"
                "\t2. Редактирование базы данных\n"
                "\t3. Базовые отношения\n"
                "\t4. Дерево предков\n"
                "\t5. Дерево потомков\n"
                "\t6. Запрос на естественном языке\n"
                "\t7. Выход\n"),
            X = rd:readStr(),
        act(X),
        !.
    maindialog().

class predicates
    act: (string) determ.
clauses
    act("1"):- !,
        write("Все записи:\n"),
        pd:printDb(),
        fail.
    act("2"):- !,
        write("Редактирование базы данных:\n"
            "2.1 Добавление факта\n"
            "2.2 Удаление факта\n"),
        X = rd:readStr(),
        act(X).
    act("2.1"):- !,
        write("Добавление новых сведений\n"),
        Add = addData::new(rd),
        Add:addNewPerson(),
        fail.
    act("2.2"):- !,
        write("Удаление сведений:\n"),
        Del = delData::new(rd),
        Del:delPerson(),
        fail.
    act("3"):- !,
        write("\nВыберите отношение: \n"
            "\t3.1 Родитель\n"
            "\t3.2 Отец\n"
            "\t3.3 Мать\n"
            "\t3.4 Муж\n"
            "\t3.5 Сын\n"
            "\t3.6 Дочь\n"
            "\t3.7 Сестра\n"
            "\t3.8 Брат\n"
            "\t3.9 Предок\n"),
        X = rd:readStr(),
        act(X),
        !.
    act("3.1"):-  !,
        write("Отношение \"родитель - ребенок\":\n"),
        rel:parent(Id1, Id2),
            pd:printRel(Id1, Id2),
        fail.
    act("3.2"):- !,
        write("Отношение \"отец - ребенок\":\n"),
        rel:father(Id1, Id2),
            pd:printRel(Id1, Id2),
        fail.
    act("3.3"):- !,
        write("Отношение \"мать - ребенок\":\n"),
        rel:mother(Id1, Id2),
            pd:printRel(Id1, Id2),
        fail.
    act("3.4"):- !,
        write("Отношение \"муж - жена\":\n"),
        rel:husband(Id1, Id2),
            pd:printRel(Id1, Id2),
        fail.
    act("3.5"):- !,
        write("Отношение \"сын - родитель\":\n"),
            rel:son(Id1, Id2),
            pd:printRel(Id1, Id2),
        fail.
    act("3.6"):- !,
        write("Отношение \"дочь - родитель\":\n"),
        rel:daughter(Id1, Id2),
            pd:printRel(Id1, Id2),
        fail.
    act("3.7"):- !,
        write("Отношение \"сестра – брат или сестра\":\n"),
            rel:sister(Id1, Id2),
            pd:printRel(Id1, Id2),
        fail.
    act("3.8"):- !,
        write("Отношение \"брат – брат или сестра\":\n"),
        rel:brother(Id1, Id2),
            pd:printRel(Id1, Id2),
        fail.
    act("3.9"):- !,
        write("Отношение \"предок - потомок\":\n"),
        rel:ancestor(Id1, Id2),
            pd:printRel(Id1, Id2),
        fail.
    act("4"):- !,
        write("Дерево предков:\n"),
        T = trees::new(rd),
        T:findAncTree(),
        fail.
    act("5"):- !,
        write("Дерево потомков:\n"),
        T = trees::new(rd),
        T:findDescTree(),
        fail.
    act("6"):- !,
        Q = query::new(rd, rel),
        Q:query(),
        fail.
    act("7"):- !,
        write("Выход. "),
        saveDb().
    act(_):-
        write("\nТакого действия нет.\n"),
        fail.

% Сохранение базы данных
class predicates
    saveDb: ().
clauses
    saveDb():-
        write("Сохранить изменения в базе данных (да/нет)? "),
        X = string::toLowerCase(rd:readStr()),
        'д' = string::frontChar(X),
        !,
        db:save().
    saveDb().

    run():-
        db := dbrel::new("family.txt"),
        db:load(),
        pd := printData::new(db),
        rd := readData::new(pd),
        rel := relation::new(db),
        mainDialog(),
        _ = readLine().

Предикат frontChar возвращает первый символ строки.

Факты-переменные db, rel, pd и rd хранят указатели на объекты классов dbrel, relation, printData и readData, соответственно.

Вывод данных на печать

Далее создается класс printData с одноименным интерфейсом, предназначенный для вывода данных на печать (листинги 12.1012.12).

properties
    db : dbrel.

predicates
    printRel: (unsigned, unsigned).
    printDb: ().
    printDb: (string Sex).
    getName: (unsigned) -> string.
    getNamesStr: (unsigned*) -> string.
constructors
    new: (dbrel).
open core, console

facts
    db : dbrel.
clauses
    new(Db):-
        db := Db.

    printRel(Id1, Id2):-
        writef("% - %\n", getName(Id1), getName(Id2)).

    printDb():-
        printDb("").

    printDb(S):-
        db:person_nd(Id, Name, Surname, Sex, IdF, IdM),
            checkSex(S, Sex),
            SpList = getSpouses(Id, Sex),
            Spouses = getNamesStr(SpList),
            writef("%-3%-10%-12%-3%-20%-20%\n", Id, Name,
                Surname, Sex, getName(IdF), getName(IdM), Spouses),
        fail.
    printDb(_).

    getName(Id) = string::format("% %", Name, Surname):-
        db:person_nd(Id, Name, Surname, _, _, _),
        !.
    getName(_) = "-".

    getNamesStr(IdL) = string::concatWithDelimiter(NL, ", "):-
        NL = list::map(IdL, {(Id) = getName(Id)}).

predicates
    checkSex: (string, string Sex) determ.
    getSpouses: (unsigned Id, string Sex) -> unsigned*.
clauses
    checkSex("", _):- !.
    checkSex(S, S).

    getSpouses(Id, dbrel::male) = [IdS || db:spouse_nd(Id, IdS)]:- !.
    getSpouses(Id, _) = [IdS || db:spouse_nd(IdS, Id)].

Считывание данных

Теперь создается класс readData с одноименным интерфейсом, который используется для получения сведений от пользователя (листинги 12.1312.15).

properties
    pd : printData.

predicates
    readName: () -> string determ.
    readSurname: () -> string determ.
    readSex: () -> string determ.
    readFather: () -> unsigned.
    readMother: () -> unsigned.
    readSpouse: (string Sex) -> unsigned.
    readStr: () -> string.
    readNonEmptyStr: () -> string determ.
    getIdFromStr: (string) -> unsigned.
    getIdFromUser: () -> unsigned determ.
constructors
    new: (printData).
open core, console

facts
    pd : printData.

clauses
    new(PD):-
        pd := PD.

    readName() = Name:-
        write("Введите имя: "),
        Name = readNonEmptyStr().

    readSurname() = Surname:-
        write("Введите фамилию: "),
        Surname = readNonEmptyStr().

    readSex() = Sex:-
        writef("Введите пол (%/%): ", dbrel::male, dbrel::female),
        Sex = string::toLowerCase(readNonEmptyStr()),
        (Sex = dbrel::male; Sex = dbrel::female),
        !.

    readFather() = IdF:-
        write("Все мужчины:\n"),
        pd:printDb(dbrel::male),
        write("Укажите номер отца (или нажмите Enter): "),
        IdF = getIdFromStr(readStr()).

    readMother() = IdM:-
        write("Все женщины:\n"),
        pd:printDb(dbrel::female),
        write("Укажите номер матери (или нажмите Enter): "),
        IdM = getIdFromStr(readStr()).

    readSpouse(Sex) = IdS:-
        S = if Sex = dbrel::male then "и" else "а" end if,
        writef("Укажите номер супруг% (или нажмите Enter): ", S),
        IdS = getIdFromStr(readStr()).

    readStr() = string::trim(readLine()).

    readNonEmptyStr() = Str:-
        Str = readStr(),
        Str <> "".

    getIdFromStr(S) = Id:-
        Id = tryToTerm(unsigned, S),
        pd:getName(Id) <> "-",
        !.
    getIdFromStr(_) = 0.

    getIdFromUser() = Id:-
        Name = readName(),
        Surname = readSurname(),
        NamesakeList = getNamesakes(Name, Surname),
        Id = getId(NamesakeList).

predicates
    getNamesakes: (string Name, string Surname) ->
        tuple{unsigned, string, string, string, unsigned, unsigned}*.
    getId: (tuple{unsigned, string, string, string, unsigned,
        unsigned}*) -> unsigned determ.
    printPersons: (tuple{unsigned, string, string, string, unsigned,
        unsigned}*).
clauses
    getNamesakes(Name, Surname) = [tuple(I, N, S, Sex, F, M) ||
        pd:db:person_nd(I, N, S, Sex, F, M),
        string::equalIgnoreCase(Name, N),
        string::equalIgnoreCase(Surname, S)].

    getId([tuple(Id, _, _, _, _, _)]) = Id:- !.
    getId([]) = _:- !,
        write("\nТаких сведений нет в базе данных\n"),
        fail.
    getId(L) = Id:-
        printPersons(L),
        write("\nУкажите номер: "),
        Id = getIdFromStr(readStr()).

    printPersons(L):-
        list::forAll(L, {(tuple(Id, Name, Surname, Sex, F, M)):-
            writef("% - % - % - % - % - %\n", Id, Name, Surname,
                Sex, pd:getName(F), pd:getName(M))}).

Предикат equalIgnoreCase проверяет, равны ли строки с точностью до регистра символов.

Добавление данных

Далее создается класс addData, который обрабатывает процедуру получения новых сведений от пользователя и запись их в базу данных (листинги 12.1612.18).

predicates
    addNewPerson: () determ.
constructors
    new: (readData).
    open core, console

facts
    rd : readData.
clauses
    new(RD):-
        rd := RD.

    addNewPerson():-
        addPerson(Id, Sex),
        addSpouse(Id, Sex).

predicates
    addPerson: (unsigned Id [out], string Sex [out]) determ.
    addSpouse: (unsigned Id, string Sex).
    addSpouse: (unsigned Id, string Sex, unsigned IdS).
clauses
    addPerson(Id, Sex):-
        Name = rd:readName(),
        Surname = rd:readSurname(),
        Sex = rd:readSex(),
        IdF = rd:readFather(),
        IdM = rd:readMother(),
        Id = rd:pd:db:getNewId(),
        rd:pd:db:setPerson(Id, Name, Surname, Sex, IdF, IdM).

    addSpouse(Id, Sex):-
        IdS = rd:readSpouse(Sex),
        addSpouse(Id, Sex, IdS).

    addSpouse(_, _, 0):- !.
    addSpouse(Id, dbrel::male, IdS):- !,
        rd:pd:db:setSpouse(Id, IdS).
    addSpouse(Id, _, IdS):-
        rd:pd:db:setSpouse(IdS, Id).

Удаление данных

После этого создается класс delData, который занимается удалением записей из базы данных (листинги 12.1912.21).

predicates
    delPerson: () determ.

Конструктор выглядит следующим образом:

constructors
    new: (readData).
    open core, console

facts
    rd : readData.
clauses
    new(RD):-
        rd := RD.

    delPerson():-
        Id = rd:getIdFromUser(),
        Sex = rd:pd:db:getSex(Id),
        rd:pd:db:delPerson(Id),
        delSpouses(Id, Sex).

predicates
    delSpouses: (unsigned Id, string Sex).
clauses
    delSpouses(Id, dbrel::male):- !,
        foreach rd:pd:db:spouse_nd(Id, IdS) do
            rd:pd:db:delSpouse(Id, IdS)
        end foreach.
    delSpouses(Id, _):-
        foreach rd:pd:db:spouse_nd(IdS, Id) do
            rd:pd:db:delSpouse(IdS, Id)
        end foreach.

Деревья предков и потомков

Для построения и вывода на печать деревьев предков и потомков создается класс trees (листинги 12.2212.24).

predicates
    findAncTree: () determ.
    findDescTree: () determ.
constructors
    new: (readData).
open core, console

facts
    rd : readData.
    rel : relation.
clauses
    new(RD):-
        rd := RD,
        rel := relation::new(rd:pd:db).

    findAncTree():-
        Id = rd:getIdFromUser(),
        AncTree = createBTree(Id),
        printBTree(AncTree).

    findDescTree():-
        Id = rd:getIdFromUser(),
        Tree = createTree(Id),
        printTree(Tree).

domains
    btree = bt(btree, unsigned, btree); nil.
    tree = t(unsigned, tree*).

predicates
    createBTree: (unsigned) -> btree.
    createTree: (unsigned) -> tree.
    printBTree: (btree).
    printBTree: (btree, charCount).
    printTree: (tree).
    printTree: (tree, charCount).
    printElem: (unsigned, charCount).
clauses
    createBTree(I) = bt(L, I, R):-
        L = if rel:father(F, I), ! then createBTree(F) else nil end if,
        R = if rel:mother(M, I), ! then createBTree(M) else nil end if.

    createTree(Id) =
        t(Id, list::map([I || rel:parent(Id, I)], {(C) = createTree(C)})).

    printBTree(Tree):-
        printBTree(Tree, 0).

    printBTree(bt(LT, Id, RT), N):-
        printBTree(LT, N + 1),
        printElem(Id, N),
        printBTree(RT, N + 1).
    printBTree(nil, _).

    printTree(Tree):-
        printTree(Tree, 0).

    printTree(t(Id, TL), N):-
        printElem(Id, N),
        list::forAll(TL, {(T):- printTree(T, N + 1)}).

    printElem(Id, N):-
        write(string::create(N, "\t"), rd:pd:getName(Id)), nl.

Запросы к базе данных

Наконец, создается класс query для обработки запросов к базе данных (листинги 12.2512.27).

predicates
    query: () determ.
constructors
    new: (readData, relation).
    open core, console

facts
    rd : readData.
    rel : relation.
clauses
    new(RD, Rel):-
        rd := RD,
        rel := Rel.

    query():-
        write("Введите запрос:\n"),
        Str = rd:readNonEmptyStr(),
        P = parser::new(rd:pd:db),
        Term = P:parse(Str),
        write(Term), nl,
        C = calculation::new(rel),
        Res = C:calc(Term),
        Answer = rd:pd:getNamesStr(Res),
        write(Answer), nl.

12.4. Синтаксический разбор запросов

Настоящий параграф посвящен разработке языка запросов к базе данных, близкого к естественному языку. В запросах представлены операции объединения, пересечения и разности множеств (найти сыновей Петра и дочерей мужчин), композиции отношений (найти сыновей предков Петра) и отрицания (найти сыновей Петра, но не сыновей Марии). Объединение представляют союзы "и" и "или", пересечение — знак запятой "," (поэтому его нельзя использовать просто как знак пунктуации), а отрицание — частица "не". Все запросы формулируются через набор основных отношений, которыми являются бинарные отношения "родитель", "ребенок", "отец", "мать", "муж", "жена", "сын", "дочь", "сестра", "брат", "предок" и "потомок", а также унарные отношения "мужчина" и "женщина". Например, найти тетушек Петра можно с помощью запроса "найти сестер родителей Петра". Без аргумента в запросе могут быть только унарные отношения. Например, нельзя попросить "найти отцов", но можно "найти отцов мужчин и отцов женщин".

Язык запросов удовлетворяет следующей грамматике:

query    ::=  conj  conjs
conj    ::=  elem  elems
elem    ::=  rel  elem  |  [не]  rel  elem  |  name  surname  |  name  |  unrel
elems    ::=  consign  elem  elems  |  none
conjs    ::=  dizsign  conj  conjs  |  none
consign ::=  [,]
dizsign    ::=  [и]  |  [или]
unrel    ::=  [мужчина]  |  [женщина]  % и изменения по падежам и числам
rel    ::=  [родитель]  |  [предок]  | … % и изменения по падежам и числам

Запросы могут быть вида:

Найти братьев мужчин и сына Петра;

Найти предков потомков предков дочерей Петра Иванова или Марию.

Найти предков Петра, но не сыновей отца отца Петра.

Программа удаляет из запроса слова, не значимые при вычислении ответа на запрос, т. е. "игнорируемые" слова. Например, из приведенных выше запросов удаляются слова "найти" и "но".

Имена и фамилии людей, а также названия отношений изменяются по падежам и числам. Поэтому при обработке запроса выполняется операция нормализации таких слов. Для отношений проверяется, является ли неизменяемая часть слова префиксом названия отношения в запросе. Например, если слово в запросе имеет префикс "сест", то оно распознается как название отношения "сестра". Отношение "ребенок" могут представлять слова как с префиксом "ребен", так и с префиксом "дет" (см. определение отношения rel в программе).

Имена обрабатываются следующим образом. Сначала проверяется, имеется ли такое имя в базе данных (с учетом регистра). Если нет, то отнимается один символ с конца слова и проверяется, является ли полученное слово префиксом некоторого имени. Затем, при необходимости, отнимается еще один символ, и т. д. Фамилии распознаются аналогичным образом, в паре с именами.

Например, запрос

Найти сестер Анны и сыновей Петра Иванова

преобразуется в терм

diz(rel("сестра", n("Анна")), rel("сын", ns("Петр", "Иванов")))

Ниже приведен класс parser с интерфейсом parser (листинги 12.2812.30).

domains
    term = diz(term, term); con(term, term); neg(term);
        rel(string, term); unrel(string); n(string); ns(string, string).

predicates
    parse: (string) -> term.
constructors
    new: (dbrel).
facts
    db: dbrel.

clauses
    new(Db):-
        db := Db.

    parse(Str) = Term:-
        L = scan(string::toLowerCase(Str)),
        L1 = list::filter(L, {(S):- not(list::isMember(S, ignor))}),
        parser(query, L1, Term, Rest),
        !,
        write(Rest), nl.
    parse(_) = n("").

predicates
    scan: (string) -> string*.
clauses
    scan(Str) = [Tok | scan(RestStr)]:-
        string::frontToken(Str, Tok, RestStr),
        !.
    scan(_) = [].

facts
    ignor : string* := ["найти", "вычислить", "но", "которые", 
        "являются", "а", "также"].

domains
    nt = query; conj; conjs; elem; elems.

predicates
    parser: (nt, string*, term [out], string* [out]) determ.
    parser: (nt, string*, term, term [out], string* [out]).
clauses
    parser(query, L, Term, Rest):-
        parser(conj, L, Term1, L1),
        parser(conjs, L1, Term1, Term, Rest).
    parser(conj, L, Term, Rest):-
        parser(elem, L, Term1, L1),
        parser(elems, L1, Term1, Term, Rest).
    parser(elem, ["не" | L], neg(Term), Rest):-
        parser(elem, L, Term, Rest),
        !.
    parser(elem, [Rel | L], unrel(Rel1), L):-
        Rel1 = norm("ur", Rel),
        !.
    parser(elem, [Rel | L], rel(Rel1, Term), Rest):-
        Rel1 = norm("r", Rel),
        !,
    parser(elem, L, Term, Rest).
    parser(elem, [Name, Surname | L], ns(Name1, Surname1), L):-
        norm(Name, Surname, Name1, Surname1),
        !.
    parser(elem, [Name | L], n(Name1), L):-
        Name1 = norm("n", Name).

    parser(conjs, [S | L], Term1, Term, Rest):-
        dizsign(S),
        parser(conj, L, Term2, L1),
        !,
        parser(conjs, L1, diz(Term1, Term2), Term, Rest).
    parser(elems, [S | L], Term1, Term, Rest):-
        consign(S),
        parser(elem, L, Term2, L1),
        !,
        parser(elems, L1, con(Term1, Term2), Term, Rest).
    parser(_, L, Term, Term, L).

facts
    consign: (string).
    dizsign: (string).
    rel: (string, string).
    urel: (string, string).
clauses
    consign(",").

    dizsign("и").
    dizsign("или").

    rel("родитель", "родител").
    rel("ребенок", "ребен").
    rel("ребенок", "дет").
    rel("отец", "отц").
    rel("отец", "отец").
    rel("мать", "мат").
    rel("муж", "муж").
    rel("жена", "жен").
    rel("сын", "сын").
    rel("дочь", "доч").
    rel("сестра", "сест").
    rel("брат", "брат").
    rel("предок", "пред").
    rel("потомок", "потом").

    urel(dbrel::male, "мужчин").
    urel(dbrel::female, "женщин").

predicates
    norm: (string, string) -> string determ.
    norm: (string, string, string [out], string [out]) determ.
    getPrefix_nd: (string) -> string nondeterm.
clauses
    getPrefix_nd(S) = S.
    getPrefix_nd(S) = getPrefix_nd(Prefix):-
        L = string::length(S), L > 2,
        string::front(S, L - 1, Prefix, _).

    norm("r", S) = NormS:-
        rel(NormS, Sub),
        string::hasPrefix(S, Sub, _),
        !.
    norm("ur", S) = NormS:-
        urel(NormS, Sub),
        string::hasPrefix(S, Sub, _),
        !.
    norm("n", S) = Name:-
        db:person_nd(_, Name, _, _, _, _),
        Name1 = string::toLowerCase(Name),
        S1 = getPrefix_nd(S), 
        string::hasPrefix(Name1, S1, _),
        !.

    norm(N, S, Name, Surname):-
        db:person_nd(_, Name, Surname, _, _, _),
        Name1 = string::toLowerCase(Name),
        SName1 = string::toLowerCase(Surname),
        N1 = getPrefix_nd(N), 
        string::hasPrefix(Name1, N1, _),
        S1 = getPrefix_nd(S), 
        string::hasPrefix(SName1, S1, _),
        !.

Предикат front возвращает префикс строки, состоящий из заданного количества символов, и остаток строки. Предикат hasPrefix проверяет, является ли заданная подстрока префиксом строки и возвращает остаток строки.

12.5. Вычисление ответов на запросы

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

Для реализации вычислений создается класс calculation с интерфейсом calculation (листинги 12.3112.33).

predicates
    calc: (parser::term) -> unsigned* determ.
constructors
    new: (relation).
    open core, parser, list

facts
    rel: relation.

clauses
    new(R):-
        rel := R.

predicates
    n: (A*) -> A*.
    calc1: (term) -> unsigned nondeterm.
clauses
    n(L) = removeDuplicates(L).

    calc1(X) = getMember_nd(calc(X)).

    calc(diz(X, Y)) = union(calc(X), calc(Y)).
    calc(con(X, Y)) = intersection(calc(X), calc(Y)).
    calc(neg(X)) = difference(L, calc(X)):-
        L = [I || rel:db:person_nd(I, _, _, _, _, _)].
    calc(parser::n(N)) = [I || rel:db:person_nd(I, N, _, _, _, _)].
    calc(ns(N, S)) = [I || rel:db:person_nd(I, N, S, _, _, _)].
    calc(unrel(Sex)) = [I || rel:db:person_nd(I, _, _, Sex, _, _)].
    calc(rel("родитель", X)) = n([I || rel:parent(I, calc1(X))]).
    calc(rel("ребенок", X)) = n([I || rel:parent(calc1(X), I)]).
    calc(rel("отец", X)) = n([I || rel:father(I, calc1(X))]).
    calc(rel("мать", X)) = n([I || rel:mother(I, calc1(X))]).
    calc(rel("муж", X)) = n([I || rel:husband(I, calc1(X))]).
    calc(rel("жена", X)) = n([I || rel:husband(calc1(X), I)]).
    calc(rel("сын", X)) = n([I || rel:son(I, calc1(X))]).
    calc(rel("дочь", X)) = n([I || rel:daughter(I, calc1(X))]).
    calc(rel("сестра", X)) = n([I || rel:sister(I, calc1(X))]).
    calc(rel("брат", X)) = n([I || rel:brother(I, calc1(X))]).
    calc(rel("предок", X)) = n([I || rel:ancestor(I, calc1(X))]).
    calc(rel("потомок", X)) = n([I || rel:ancestor(calc1(X), I)]).

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

В упражнениях 4 – 8 (см. ниже) требуется создать базу данных, а также придумать и реализовать язык запросов к ней, близкий к естественному языку.

Упражнения

  • Добавьте в базу данных сведения о годах жизни людей. Добавьте запросы, связанные с годами жизни.
  • Добавьте в базу данных и в язык запросов отношения свойства.
  • Добавьте в базу данных и в язык запросов сведения о роде занятий и о месте жительства людей.
  • База данных "Династия" (Романовых, Рюриковичей или др.).
  • База данных "География России", описывающая взаимоотношения между объектами некоторой области.
  • База данных "Биология", описывающая взаимоотношения между растениями некоторого семейства.
  • База данных "Естественные языки", описывающая взаимоотношения между языками в некоторой группе языков.
  • База данных "Языки программирования", описывающая взаимоотношения между языками в некоторой парадигме.
  • Страницы:

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

    В данной главе создается консольный проект в объектном стиле.

    Язык Visual Prolog — объектно-ориентированный. Классы и интерфейсы создаются с помощью диалогового окна Create Project Item. Классы могут иметь интерфейс, а могут не иметь интерфейса. Объекты порождают те, и только те классы, которые имеют интерфейс.

    Декларация класса помещается в файл с расширением cl, интерфейс — в файл с расширением i, имплементация — в файл с расширением pro.

    Рассмотрим пример интерфейса, декларации класса с этим интерфейсом и его имплементации.

    interface person      % интерфейс
        open core
    
        properties
            id : unsigned.
            name : string.
            age : positive.
    end interface person
    %---------------------------------------------------------------------------
    class person : person      % декларация класса
        open core
    
        constructors        % объявление конструктора
            new: (unsigned Id).
    
        predicates
            count: () -> positive.
    end class student
    %---------------------------------------------------------------------------
    implement person        % имплементация класса
        open core
    
        facts
            id : unsigned.
            name : string := "".
            age : positive := 18.
        
        class facts
            counter : positive := 0.
    
        clauses
            new(Id):-
                id := Id,
                counter := counter + 1.
    
            count() = counter.
    end implement person
    %---------------------------------------------------------------------------
    goal            % цель программы
        mainExe::run(main::run),
        P1 = person::new(1),
        P1:name := "Вася",
        P1:age := 18,
        P2 = person::new(2),
        P2:name := "Маша",
        P2:age := 19,
        stdio::writef("%. % - %\n%. % - %\nКоличество = %",
            P1:id, P1:name, P1:age, P2:id, P2:name, P2:age,
            person::count()).
    

    В разделе properties объявляются свойства. Обычно они определяются в имплементации класса с помощью фактов-переменных (см. выше). Определение свойства может выглядеть и следующим образом:

    facts
        n: positive.
    clauses
        age() = n:-
            stdio::writef("Считано значение свойства age = %\n", n).
        age(V):-
            n := V,
            stdio::writef("Свойству age присвоено значение %\n", n).
    

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

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

    Подробнее об объектно-ориентированном программировании в языке Visual Prolog см. [17].

    12.1. База данных

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

    Для реализации доступа к сведениям, хранящимся в базе данных, создается класс dbrel. Объект базы данных создается по имени файла, в котором хранятся факты базы данных.

    В базе данных хранятся сведения об именах и фамилиях людей, их поле, отце, матери и супругах. Каждый человек имеет уникальный идентификатор. При записи сведений о новом человеке ему автоматически присваивается идентификатор. Для хранения записей используется два отношения — person/6 и spouse/2.

    Ниже приведен интерфейс общения с базой данных (файл dbrel.i).

    constants
        male = "м".
        female = "ж".
    
    predicates
        load: ().
        save: ().
        person_nd: (unsigned, string, string, string, unsigned, unsigned)
            nondeterm (o,o,o,o,o,o) (o,i,o,o,o,o) (o,i,i,o,o,o) (o,o,o,i,o,o)
               (o,o,o,o,i,o) (i,o,o,o,o,o) (i,o,o,o,i,o) (o,o,o,o,o,i) (i,o,o,o,o,i).
        spouse_nd: (unsigned, unsigned) nondeterm (o,o) (i,o) (o,i).
        getSex: (unsigned Id) -> string determ.
        getNewId: () -> unsigned.
        setPerson: (unsigned, string, string, string, unsigned, unsigned).
        setSpouse: (unsigned, unsigned).
        delPerson: (unsigned).
        delSpouse: (unsigned, unsigned).
    

    Конструктор объявляется так, как показано ниже (файл dbrel.cl).

    constructors
        new: (string FileName).
    

    Имплементация приведена в листинге 12.4 (файл dbrel.pro).

    facts
        filename : string.
        maxId : unsigned := 0.
    clauses
        new(FileName):-
            filename := FileName.
    
    facts - rel
        person: (unsigned, string, string, string, unsigned, unsigned).
        spouse: (unsigned IdHusband, unsigned IdWife).
    
    clauses
        load():-
            try file::consult(filename, rel)
            catch Error do
                writef("Error %. Unable to load the database from "
                    "the file %\n", Error, filename)
            end try,
            setMaxId().
            
        save():-
            file::save(filename, rel),
            write("\nИзменения сохранены в базы данных.").
    
        person_nd(Id, Name, Surname, Sex, IdF, IdM):-
            person(Id, Name, Surname, Sex, IdF, IdM).
    
        spouse_nd(IdH, IdW):-
            spouse(IdH, IdW).
    
        getSex(Id) = Sex:-
            person(Id, _, _, Sex, _, _),
            !.
    
        getNewId() = maxId:-
            maxId := maxId + 1.
    
        setPerson(Id, Name, Surname, Sex, IdF, IdM):-
            assert(person(Id, Name, Surname, Sex, IdF, IdM)),
            writef("Запись '% - % - % - % - % - %' добавлена\n",
                Id, Name, Surname, Sex, IdF, IdM).
    
        setSpouse(IdH, IdW):-
            assert(spouse(IdH, IdW)),
            writef("Запись '% - %' добавлена\n", IdH, IdW).
    
        delPerson(Id):-
            retractAll(person(Id, _, _, _, _, _)),
            writef("Записи о % удалены\n", Id).
    
        delSpouse(IdH, IdW):-
            retractAll(spouse(IdH, IdW)),
            writef("Запись '% - %' удалена\n", IdH, IdW).
    
    predicates
        setMaxId: ().
    clauses
        setMaxId():-
            L = [Id || person(Id, _, _, _, _, _)],
            L <> [],
            !,
            maxId := list::maximum(L).
        setMaxId().
    

    12.2. Определение основных отношений

    В настоящем параграфе определяются основные родственные отношения.

    Создается класс relation для описания родственных отношений. Аргументами всех отношений являются идентификаторы людей. Ниже приведен интерфейс relation (файл relation.i).

    properties
        db : dbrel.
    
    predicates
        parent: (unsigned, unsigned) nondeterm (o,i) (o,o) (i,o) (i,i).
        father: (unsigned, unsigned) nondeterm (o,i) (o,o) (i,o) (i,i).
        mother: (unsigned, unsigned) nondeterm (o,i) (o,o) (i,o) (i,i).
        husband: (unsigned, unsigned) nondeterm (o,i) (i,o) (o,o).
        son: (unsigned, unsigned) nondeterm (o,i) (o,o).
        daughter: (unsigned, unsigned) nondeterm (o,i) (o,o).
        sister: (unsigned, unsigned) nondeterm (o,i) (o,o).
        brother: (unsigned, unsigned) nondeterm (o,i) (o,o).
        ancestor: (unsigned, unsigned) nondeterm (o,i) (i,o) (o,o) (i,i).
    

    Объявление конструктора приведено в листинге 12.6 (файл relation.cl).

    constructors
        new: (dbrel).
    

    Ниже приведена имплементация (файл relation.pro).

    facts
        db: dbrel.
    clauses
        new(Db):-
            db := Db.
    
    predicates
        male: (unsigned [out]) nondeterm.
        female: (unsigned [out]) nondeterm.
    clauses
        male(Id):-
            db:person_nd(Id, _, _, dbrel::male, _, _).
    
        female(Id):-
            db:person_nd(Id, _, _, dbrel::female, _, _).
    
    clauses
        father(IdF, IdCh):-
            db:person_nd(IdCh, _, _, _, IdF, _),
            IdF > 0.
    
        mother(IdM, IdCh):-
            db:person_nd(IdCh, _, _, _, _, IdM),
            IdM > 0.
    
        parent(IdP, IdCh):-
            father(IdP, IdCh);
            mother(IdP, IdCh).
    
        husband(IdH, IdW):-
            db:spouse_nd(IdH, IdW).
    
         son(IdS, IdP):-
            male(IdS),
            parent(IdP, IdS).
    
        daughter(IdD, IdP):-
            female(IdD),
            parent(IdP, IdD).
    
        ancestor(IdA, IdD):-
            parent(IdA, IdD).
        ancestor(IdA, IdD):-
            parent(IdA, X),
            ancestor(X, IdD).
    
    predicates
        sister_nd: (unsigned, unsigned) nondeterm (o,i) (o,o).
        brother_nd: (unsigned, unsigned) nondeterm (o,i) (o,o).
        get_nd: (A*) -> A nondeterm.
    clauses
        sister_nd(IdS, IdSib):-
            female(IdS),
            parent(P, IdS),
            parent(P, IdSib),
            IdS <> IdSib.
    
        brother_nd(IdB, IdSib):-
            male(IdB),
            parent(P, IdB),
            parent(P, IdSib),
            IdB <> IdSib.
    
        brother(IdB, Id):-
            free(Id),
            tuple(IdB, Id) = get_nd([tuple(I, I1) || brother_nd(I, I1)]).
        brother(IdB, Id):-
            bound(Id),
            IdB = get_nd([I || brother_nd(I, Id)]).
    
        sister(IdS, Id):-
            free(Id),
            tuple(IdS, Id) = get_nd([tuple(I, I1) || sister_nd(I, I1)]).
        sister(IdS, Id):-
            bound(Id),
            IdS = get_nd([I || sister_nd(I, Id)]).
    
        get_nd(L) = list::getMember_nd(list::removeDuplicates(L)).
    

    Предикат free/1 истинен, если его аргумент является свободной переменной, а предикат bound/1, соответственно, если конкретизированной.

    12.3. Диалог с пользователем

    В настоящем параграфе рассматривается реализация общения пользователя с компьютером.

    Создается модуль mainDialog для реализации диалога с базой данных. Во время диалога пользователь может просмотреть базу данных, отредактировать базу данных (добавить или удалить факты) и просмотреть основные родственные отношения. Например, отношение "родитель – ребенок" имеет вид:

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

    Кроме этого, пользователь может посмотреть на дерево предков и на дерево потомков человека, а также задать запрос к базе данных на языке, приближенном к естественному языку. Запросы должны удовлетворять определенной грамматике. Для организации диалога используется repeat-цикл. Выход из цикла выполняется по желанию пользователя о выходе из программы. Перед выходом программа спрашивает, сохранить ли изменения в базе данных. Если пользователь не дает положительный ответ, то изменения не сохраняются.

    В декларацию класса mainDialog (листинг 12.8) следует поместить объявление предиката, который вызывается в разделе goal (файл mainDialog.cl).

    predicates
        run: ().
    

    Раздел goal проекта должен быть изменен следующим образом (файл main.pro):

    goal
        mainExe::run(main::run),
        mainDialog::run().
    

    Имплементация класса mainDialog приведена ниже (файл mainDialog.pro). Указатели на объекты классов dbrel, relation, printData и readData (они описываются ниже) хранятся в фактах-переменных. При инициализации им присваивается "значение" erroneous, что по-существу, означает, что значение им присвоено не было. Это "значение" можно присваивать динамически, кроме этого, имеется предикат проверки isErroneous(имя_факта), что часто оказывается полезным в GUI-приложениях.

    open core, console
    
    class facts
        db : dbrel := erroneous.
        rel : relation := erroneous.
        pd : printData := erroneous.
        rd : readData := erroneous.
    
    class predicates
        maindialog: ().
    clauses
        maindialog():-
            std::repeat(),
                write("\nВыберите действие:\n"
                    "\t1. Просмотр базы данных\n"
                    "\t2. Редактирование базы данных\n"
                    "\t3. Базовые отношения\n"
                    "\t4. Дерево предков\n"
                    "\t5. Дерево потомков\n"
                    "\t6. Запрос на естественном языке\n"
                    "\t7. Выход\n"),
                X = rd:readStr(),
            act(X),
            !.
        maindialog().
    
    class predicates
        act: (string) determ.
    clauses
        act("1"):- !,
            write("Все записи:\n"),
            pd:printDb(),
            fail.
        act("2"):- !,
            write("Редактирование базы данных:\n"
                "2.1 Добавление факта\n"
                "2.2 Удаление факта\n"),
            X = rd:readStr(),
            act(X).
        act("2.1"):- !,
            write("Добавление новых сведений\n"),
            Add = addData::new(rd),
            Add:addNewPerson(),
            fail.
        act("2.2"):- !,
            write("Удаление сведений:\n"),
            Del = delData::new(rd),
            Del:delPerson(),
            fail.
        act("3"):- !,
            write("\nВыберите отношение: \n"
                "\t3.1 Родитель\n"
                "\t3.2 Отец\n"
                "\t3.3 Мать\n"
                "\t3.4 Муж\n"
                "\t3.5 Сын\n"
                "\t3.6 Дочь\n"
                "\t3.7 Сестра\n"
                "\t3.8 Брат\n"
                "\t3.9 Предок\n"),
            X = rd:readStr(),
            act(X),
            !.
        act("3.1"):-  !,
            write("Отношение \"родитель - ребенок\":\n"),
            rel:parent(Id1, Id2),
                pd:printRel(Id1, Id2),
            fail.
        act("3.2"):- !,
            write("Отношение \"отец - ребенок\":\n"),
            rel:father(Id1, Id2),
                pd:printRel(Id1, Id2),
            fail.
        act("3.3"):- !,
            write("Отношение \"мать - ребенок\":\n"),
            rel:mother(Id1, Id2),
                pd:printRel(Id1, Id2),
            fail.
        act("3.4"):- !,
            write("Отношение \"муж - жена\":\n"),
            rel:husband(Id1, Id2),
                pd:printRel(Id1, Id2),
            fail.
        act("3.5"):- !,
            write("Отношение \"сын - родитель\":\n"),
                rel:son(Id1, Id2),
                pd:printRel(Id1, Id2),
            fail.
        act("3.6"):- !,
            write("Отношение \"дочь - родитель\":\n"),
            rel:daughter(Id1, Id2),
                pd:printRel(Id1, Id2),
            fail.
        act("3.7"):- !,
            write("Отношение \"сестра – брат или сестра\":\n"),
                rel:sister(Id1, Id2),
                pd:printRel(Id1, Id2),
            fail.
        act("3.8"):- !,
            write("Отношение \"брат – брат или сестра\":\n"),
            rel:brother(Id1, Id2),
                pd:printRel(Id1, Id2),
            fail.
        act("3.9"):- !,
            write("Отношение \"предок - потомок\":\n"),
            rel:ancestor(Id1, Id2),
                pd:printRel(Id1, Id2),
            fail.
        act("4"):- !,
            write("Дерево предков:\n"),
            T = trees::new(rd),
            T:findAncTree(),
            fail.
        act("5"):- !,
            write("Дерево потомков:\n"),
            T = trees::new(rd),
            T:findDescTree(),
            fail.
        act("6"):- !,
            Q = query::new(rd, rel),
            Q:query(),
            fail.
        act("7"):- !,
            write("Выход. "),
            saveDb().
        act(_):-
            write("\nТакого действия нет.\n"),
            fail.
    
    % Сохранение базы данных
    class predicates
        saveDb: ().
    clauses
        saveDb():-
            write("Сохранить изменения в базе данных (да/нет)? "),
            X = string::toLowerCase(rd:readStr()),
            'д' = string::frontChar(X),
            !,
            db:save().
        saveDb().
    
        run():-
            db := dbrel::new("family.txt"),
            db:load(),
            pd := printData::new(db),
            rd := readData::new(pd),
            rel := relation::new(db),
            mainDialog(),
            _ = readLine().
    

    Предикат frontChar возвращает первый символ строки.

    Факты-переменные db, rel, pd и rd хранят указатели на объекты классов dbrel, relation, printData и readData, соответственно.

    Вывод данных на печать

    Далее создается класс printData с одноименным интерфейсом, предназначенный для вывода данных на печать (листинги 12.1012.12).

    properties
        db : dbrel.
    
    predicates
        printRel: (unsigned, unsigned).
        printDb: ().
        printDb: (string Sex).
        getName: (unsigned) -> string.
        getNamesStr: (unsigned*) -> string.
    
    constructors
        new: (dbrel).
    
    open core, console
    
    facts
        db : dbrel.
    clauses
        new(Db):-
            db := Db.
    
        printRel(Id1, Id2):-
            writef("% - %\n", getName(Id1), getName(Id2)).
    
        printDb():-
            printDb("").
    
        printDb(S):-
            db:person_nd(Id, Name, Surname, Sex, IdF, IdM),
                checkSex(S, Sex),
                SpList = getSpouses(Id, Sex),
                Spouses = getNamesStr(SpList),
                writef("%-3%-10%-12%-3%-20%-20%\n", Id, Name,
                    Surname, Sex, getName(IdF), getName(IdM), Spouses),
            fail.
        printDb(_).
    
        getName(Id) = string::format("% %", Name, Surname):-
            db:person_nd(Id, Name, Surname, _, _, _),
            !.
        getName(_) = "-".
    
        getNamesStr(IdL) = string::concatWithDelimiter(NL, ", "):-
            NL = list::map(IdL, {(Id) = getName(Id)}).
    
    predicates
        checkSex: (string, string Sex) determ.
        getSpouses: (unsigned Id, string Sex) -> unsigned*.
    clauses
        checkSex("", _):- !.
        checkSex(S, S).
    
        getSpouses(Id, dbrel::male) = [IdS || db:spouse_nd(Id, IdS)]:- !.
        getSpouses(Id, _) = [IdS || db:spouse_nd(IdS, Id)].
    

    Считывание данных

    Теперь создается класс readData с одноименным интерфейсом, который используется для получения сведений от пользователя (листинги 12.1312.15).

    properties
        pd : printData.
    
    predicates
        readName: () -> string determ.
        readSurname: () -> string determ.
        readSex: () -> string determ.
        readFather: () -> unsigned.
        readMother: () -> unsigned.
        readSpouse: (string Sex) -> unsigned.
        readStr: () -> string.
        readNonEmptyStr: () -> string determ.
        getIdFromStr: (string) -> unsigned.
        getIdFromUser: () -> unsigned determ.
    
    constructors
        new: (printData).
    
    open core, console
    
    facts
        pd : printData.
    
    clauses
        new(PD):-
            pd := PD.
    
        readName() = Name:-
            write("Введите имя: "),
            Name = readNonEmptyStr().
    
        readSurname() = Surname:-
            write("Введите фамилию: "),
            Surname = readNonEmptyStr().
    
        readSex() = Sex:-
            writef("Введите пол (%/%): ", dbrel::male, dbrel::female),
            Sex = string::toLowerCase(readNonEmptyStr()),
            (Sex = dbrel::male; Sex = dbrel::female),
            !.
    
        readFather() = IdF:-
            write("Все мужчины:\n"),
            pd:printDb(dbrel::male),
            write("Укажите номер отца (или нажмите Enter): "),
            IdF = getIdFromStr(readStr()).
    
        readMother() = IdM:-
            write("Все женщины:\n"),
            pd:printDb(dbrel::female),
            write("Укажите номер матери (или нажмите Enter): "),
            IdM = getIdFromStr(readStr()).
    
        readSpouse(Sex) = IdS:-
            S = if Sex = dbrel::male then "и" else "а" end if,
            writef("Укажите номер супруг% (или нажмите Enter): ", S),
            IdS = getIdFromStr(readStr()).
    
        readStr() = string::trim(readLine()).
    
        readNonEmptyStr() = Str:-
            Str = readStr(),
            Str <> "".
    
        getIdFromStr(S) = Id:-
            Id = tryToTerm(unsigned, S),
            pd:getName(Id) <> "-",
            !.
        getIdFromStr(_) = 0.
    
        getIdFromUser() = Id:-
            Name = readName(),
            Surname = readSurname(),
            NamesakeList = getNamesakes(Name, Surname),
            Id = getId(NamesakeList).
    
    predicates
        getNamesakes: (string Name, string Surname) ->
            tuple{unsigned, string, string, string, unsigned, unsigned}*.
        getId: (tuple{unsigned, string, string, string, unsigned,
            unsigned}*) -> unsigned determ.
        printPersons: (tuple{unsigned, string, string, string, unsigned,
            unsigned}*).
    clauses
        getNamesakes(Name, Surname) = [tuple(I, N, S, Sex, F, M) ||
            pd:db:person_nd(I, N, S, Sex, F, M),
            string::equalIgnoreCase(Name, N),
            string::equalIgnoreCase(Surname, S)].
    
        getId([tuple(Id, _, _, _, _, _)]) = Id:- !.
        getId([]) = _:- !,
            write("\nТаких сведений нет в базе данных\n"),
            fail.
        getId(L) = Id:-
            printPersons(L),
            write("\nУкажите номер: "),
            Id = getIdFromStr(readStr()).
    
        printPersons(L):-
            list::forAll(L, {(tuple(Id, Name, Surname, Sex, F, M)):-
                writef("% - % - % - % - % - %\n", Id, Name, Surname,
                    Sex, pd:getName(F), pd:getName(M))}).
    

    Предикат equalIgnoreCase проверяет, равны ли строки с точностью до регистра символов.

    Добавление данных

    Далее создается класс addData, который обрабатывает процедуру получения новых сведений от пользователя и запись их в базу данных (листинги 12.1612.18).

    predicates
        addNewPerson: () determ.
    
    constructors
        new: (readData).
    
        open core, console
    
    facts
        rd : readData.
    clauses
        new(RD):-
            rd := RD.
    
        addNewPerson():-
            addPerson(Id, Sex),
            addSpouse(Id, Sex).
    
    predicates
        addPerson: (unsigned Id [out], string Sex [out]) determ.
        addSpouse: (unsigned Id, string Sex).
        addSpouse: (unsigned Id, string Sex, unsigned IdS).
    clauses
        addPerson(Id, Sex):-
            Name = rd:readName(),
            Surname = rd:readSurname(),
            Sex = rd:readSex(),
            IdF = rd:readFather(),
            IdM = rd:readMother(),
            Id = rd:pd:db:getNewId(),
            rd:pd:db:setPerson(Id, Name, Surname, Sex, IdF, IdM).
    
        addSpouse(Id, Sex):-
            IdS = rd:readSpouse(Sex),
            addSpouse(Id, Sex, IdS).
    
        addSpouse(_, _, 0):- !.
        addSpouse(Id, dbrel::male, IdS):- !,
            rd:pd:db:setSpouse(Id, IdS).
        addSpouse(Id, _, IdS):-
            rd:pd:db:setSpouse(IdS, Id).
    

    Удаление данных

    После этого создается класс delData, который занимается удалением записей из базы данных (листинги 12.1912.21).

    predicates
        delPerson: () determ.
    

    Конструктор выглядит следующим образом:

    constructors
        new: (readData).
    
        open core, console
    
    facts
        rd : readData.
    clauses
        new(RD):-
            rd := RD.
    
        delPerson():-
            Id = rd:getIdFromUser(),
            Sex = rd:pd:db:getSex(Id),
            rd:pd:db:delPerson(Id),
            delSpouses(Id, Sex).
    
    predicates
        delSpouses: (unsigned Id, string Sex).
    clauses
        delSpouses(Id, dbrel::male):- !,
            foreach rd:pd:db:spouse_nd(Id, IdS) do
                rd:pd:db:delSpouse(Id, IdS)
            end foreach.
        delSpouses(Id, _):-
            foreach rd:pd:db:spouse_nd(IdS, Id) do
                rd:pd:db:delSpouse(IdS, Id)
            end foreach.
    

    Деревья предков и потомков

    Для построения и вывода на печать деревьев предков и потомков создается класс trees (листинги 12.2212.24).

    predicates
        findAncTree: () determ.
        findDescTree: () determ.
    
    constructors
        new: (readData).
    
    open core, console
    
    facts
        rd : readData.
        rel : relation.
    clauses
        new(RD):-
            rd := RD,
            rel := relation::new(rd:pd:db).
    
        findAncTree():-
            Id = rd:getIdFromUser(),
            AncTree = createBTree(Id),
            printBTree(AncTree).
    
        findDescTree():-
            Id = rd:getIdFromUser(),
            Tree = createTree(Id),
            printTree(Tree).
    
    domains
        btree = bt(btree, unsigned, btree); nil.
        tree = t(unsigned, tree*).
    
    predicates
        createBTree: (unsigned) -> btree.
        createTree: (unsigned) -> tree.
        printBTree: (btree).
        printBTree: (btree, charCount).
        printTree: (tree).
        printTree: (tree, charCount).
        printElem: (unsigned, charCount).
    clauses
        createBTree(I) = bt(L, I, R):-
            L = if rel:father(F, I), ! then createBTree(F) else nil end if,
            R = if rel:mother(M, I), ! then createBTree(M) else nil end if.
    
        createTree(Id) =
            t(Id, list::map([I || rel:parent(Id, I)], {(C) = createTree(C)})).
    
        printBTree(Tree):-
            printBTree(Tree, 0).
    
        printBTree(bt(LT, Id, RT), N):-
            printBTree(LT, N + 1),
            printElem(Id, N),
            printBTree(RT, N + 1).
        printBTree(nil, _).
    
        printTree(Tree):-
            printTree(Tree, 0).
    
        printTree(t(Id, TL), N):-
            printElem(Id, N),
            list::forAll(TL, {(T):- printTree(T, N + 1)}).
    
        printElem(Id, N):-
            write(string::create(N, "\t"), rd:pd:getName(Id)), nl.
    

    Запросы к базе данных

    Наконец, создается класс query для обработки запросов к базе данных (листинги 12.2512.27).

    predicates
        query: () determ.
    
    constructors
        new: (readData, relation).
    
        open core, console
    
    facts
        rd : readData.
        rel : relation.
    clauses
        new(RD, Rel):-
            rd := RD,
            rel := Rel.
    
        query():-
            write("Введите запрос:\n"),
            Str = rd:readNonEmptyStr(),
            P = parser::new(rd:pd:db),
            Term = P:parse(Str),
            write(Term), nl,
            C = calculation::new(rel),
            Res = C:calc(Term),
            Answer = rd:pd:getNamesStr(Res),
            write(Answer), nl.
    

    12.4. Синтаксический разбор запросов

    Настоящий параграф посвящен разработке языка запросов к базе данных, близкого к естественному языку. В запросах представлены операции объединения, пересечения и разности множеств (найти сыновей Петра и дочерей мужчин), композиции отношений (найти сыновей предков Петра) и отрицания (найти сыновей Петра, но не сыновей Марии). Объединение представляют союзы "и" и "или", пересечение — знак запятой "," (поэтому его нельзя использовать просто как знак пунктуации), а отрицание — частица "не". Все запросы формулируются через набор основных отношений, которыми являются бинарные отношения "родитель", "ребенок", "отец", "мать", "муж", "жена", "сын", "дочь", "сестра", "брат", "предок" и "потомок", а также унарные отношения "мужчина" и "женщина". Например, найти тетушек Петра можно с помощью запроса "найти сестер родителей Петра". Без аргумента в запросе могут быть только унарные отношения. Например, нельзя попросить "найти отцов", но можно "найти отцов мужчин и отцов женщин".

    Язык запросов удовлетворяет следующей грамматике:

    query    ::=  conj  conjs
    conj    ::=  elem  elems
    elem    ::=  rel  elem  |  [не]  rel  elem  |  name  surname  |  name  |  unrel
    elems    ::=  consign  elem  elems  |  none
    conjs    ::=  dizsign  conj  conjs  |  none
    consign ::=  [,]
    dizsign    ::=  [и]  |  [или]
    unrel    ::=  [мужчина]  |  [женщина]  % и изменения по падежам и числам
    rel    ::=  [родитель]  |  [предок]  | … % и изменения по падежам и числам
    

    Запросы могут быть вида:

    Найти братьев мужчин и сына Петра;

    Найти предков потомков предков дочерей Петра Иванова или Марию.

    Найти предков Петра, но не сыновей отца отца Петра.

    Программа удаляет из запроса слова, не значимые при вычислении ответа на запрос, т. е. "игнорируемые" слова. Например, из приведенных выше запросов удаляются слова "найти" и "но".

    Имена и фамилии людей, а также названия отношений изменяются по падежам и числам. Поэтому при обработке запроса выполняется операция нормализации таких слов. Для отношений проверяется, является ли неизменяемая часть слова префиксом названия отношения в запросе. Например, если слово в запросе имеет префикс "сест", то оно распознается как название отношения "сестра". Отношение "ребенок" могут представлять слова как с префиксом "ребен", так и с префиксом "дет" (см. определение отношения rel в программе).

    Имена обрабатываются следующим образом. Сначала проверяется, имеется ли такое имя в базе данных (с учетом регистра). Если нет, то отнимается один символ с конца слова и проверяется, является ли полученное слово префиксом некоторого имени. Затем, при необходимости, отнимается еще один символ, и т. д. Фамилии распознаются аналогичным образом, в паре с именами.

    Например, запрос

    Найти сестер Анны и сыновей Петра Иванова

    преобразуется в терм

    diz(rel("сестра", n("Анна")), rel("сын", ns("Петр", "Иванов")))
    

    Ниже приведен класс parser с интерфейсом parser (листинги 12.2812.30).

    domains
        term = diz(term, term); con(term, term); neg(term);
            rel(string, term); unrel(string); n(string); ns(string, string).
    
    predicates
        parse: (string) -> term.
    
    constructors
        new: (dbrel).
    
    facts
        db: dbrel.
    
    clauses
        new(Db):-
            db := Db.
    
        parse(Str) = Term:-
            L = scan(string::toLowerCase(Str)),
            L1 = list::filter(L, {(S):- not(list::isMember(S, ignor))}),
            parser(query, L1, Term, Rest),
            !,
            write(Rest), nl.
        parse(_) = n("").
    
    predicates
        scan: (string) -> string*.
    clauses
        scan(Str) = [Tok | scan(RestStr)]:-
            string::frontToken(Str, Tok, RestStr),
            !.
        scan(_) = [].
    
    facts
        ignor : string* := ["найти", "вычислить", "но", "которые", 
            "являются", "а", "также"].
    
    domains
        nt = query; conj; conjs; elem; elems.
    
    predicates
        parser: (nt, string*, term [out], string* [out]) determ.
        parser: (nt, string*, term, term [out], string* [out]).
    clauses
        parser(query, L, Term, Rest):-
            parser(conj, L, Term1, L1),
            parser(conjs, L1, Term1, Term, Rest).
        parser(conj, L, Term, Rest):-
            parser(elem, L, Term1, L1),
            parser(elems, L1, Term1, Term, Rest).
        parser(elem, ["не" | L], neg(Term), Rest):-
            parser(elem, L, Term, Rest),
            !.
        parser(elem, [Rel | L], unrel(Rel1), L):-
            Rel1 = norm("ur", Rel),
            !.
        parser(elem, [Rel | L], rel(Rel1, Term), Rest):-
            Rel1 = norm("r", Rel),
            !,
        parser(elem, L, Term, Rest).
        parser(elem, [Name, Surname | L], ns(Name1, Surname1), L):-
            norm(Name, Surname, Name1, Surname1),
            !.
        parser(elem, [Name | L], n(Name1), L):-
            Name1 = norm("n", Name).
    
        parser(conjs, [S | L], Term1, Term, Rest):-
            dizsign(S),
            parser(conj, L, Term2, L1),
            !,
            parser(conjs, L1, diz(Term1, Term2), Term, Rest).
        parser(elems, [S | L], Term1, Term, Rest):-
            consign(S),
            parser(elem, L, Term2, L1),
            !,
            parser(elems, L1, con(Term1, Term2), Term, Rest).
        parser(_, L, Term, Term, L).
    
    facts
        consign: (string).
        dizsign: (string).
        rel: (string, string).
        urel: (string, string).
    clauses
        consign(",").
    
        dizsign("и").
        dizsign("или").
    
        rel("родитель", "родител").
        rel("ребенок", "ребен").
        rel("ребенок", "дет").
        rel("отец", "отц").
        rel("отец", "отец").
        rel("мать", "мат").
        rel("муж", "муж").
        rel("жена", "жен").
        rel("сын", "сын").
        rel("дочь", "доч").
        rel("сестра", "сест").
        rel("брат", "брат").
        rel("предок", "пред").
        rel("потомок", "потом").
    
        urel(dbrel::male, "мужчин").
        urel(dbrel::female, "женщин").
    
    predicates
        norm: (string, string) -> string determ.
        norm: (string, string, string [out], string [out]) determ.
        getPrefix_nd: (string) -> string nondeterm.
    clauses
        getPrefix_nd(S) = S.
        getPrefix_nd(S) = getPrefix_nd(Prefix):-
            L = string::length(S), L > 2,
            string::front(S, L - 1, Prefix, _).
    
        norm("r", S) = NormS:-
            rel(NormS, Sub),
            string::hasPrefix(S, Sub, _),
            !.
        norm("ur", S) = NormS:-
            urel(NormS, Sub),
            string::hasPrefix(S, Sub, _),
            !.
        norm("n", S) = Name:-
            db:person_nd(_, Name, _, _, _, _),
            Name1 = string::toLowerCase(Name),
            S1 = getPrefix_nd(S), 
            string::hasPrefix(Name1, S1, _),
            !.
    
        norm(N, S, Name, Surname):-
            db:person_nd(_, Name, Surname, _, _, _),
            Name1 = string::toLowerCase(Name),
            SName1 = string::toLowerCase(Surname),
            N1 = getPrefix_nd(N), 
            string::hasPrefix(Name1, N1, _),
            S1 = getPrefix_nd(S), 
            string::hasPrefix(SName1, S1, _),
            !.
    

    Предикат front возвращает префикс строки, состоящий из заданного количества символов, и остаток строки. Предикат hasPrefix проверяет, является ли заданная подстрока префиксом строки и возвращает остаток строки.

    12.5. Вычисление ответов на запросы

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

    Для реализации вычислений создается класс calculation с интерфейсом calculation (листинги 12.3112.33).

    predicates
        calc: (parser::term) -> unsigned* determ.
    
    constructors
        new: (relation).
    
        open core, parser, list
    
    facts
        rel: relation.
    
    clauses
        new(R):-
            rel := R.
    
    predicates
        n: (A*) -> A*.
        calc1: (term) -> unsigned nondeterm.
    clauses
        n(L) = removeDuplicates(L).
    
        calc1(X) = getMember_nd(calc(X)).
    
        calc(diz(X, Y)) = union(calc(X), calc(Y)).
        calc(con(X, Y)) = intersection(calc(X), calc(Y)).
        calc(neg(X)) = difference(L, calc(X)):-
            L = [I || rel:db:person_nd(I, _, _, _, _, _)].
        calc(parser::n(N)) = [I || rel:db:person_nd(I, N, _, _, _, _)].
        calc(ns(N, S)) = [I || rel:db:person_nd(I, N, S, _, _, _)].
        calc(unrel(Sex)) = [I || rel:db:person_nd(I, _, _, Sex, _, _)].
        calc(rel("родитель", X)) = n([I || rel:parent(I, calc1(X))]).
        calc(rel("ребенок", X)) = n([I || rel:parent(calc1(X), I)]).
        calc(rel("отец", X)) = n([I || rel:father(I, calc1(X))]).
        calc(rel("мать", X)) = n([I || rel:mother(I, calc1(X))]).
        calc(rel("муж", X)) = n([I || rel:husband(I, calc1(X))]).
        calc(rel("жена", X)) = n([I || rel:husband(calc1(X), I)]).
        calc(rel("сын", X)) = n([I || rel:son(I, calc1(X))]).
        calc(rel("дочь", X)) = n([I || rel:daughter(I, calc1(X))]).
        calc(rel("сестра", X)) = n([I || rel:sister(I, calc1(X))]).
        calc(rel("брат", X)) = n([I || rel:brother(I, calc1(X))]).
        calc(rel("предок", X)) = n([I || rel:ancestor(I, calc1(X))]).
        calc(rel("потомок", X)) = n([I || rel:ancestor(calc1(X), I)]).
    

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

    В упражнениях 4 – 8 (см. ниже) требуется создать базу данных, а также придумать и реализовать язык запросов к ней, близкий к естественному языку.

    Упражнения

  • Добавьте в базу данных сведения о годах жизни людей. Добавьте запросы, связанные с годами жизни.
  • Добавьте в базу данных и в язык запросов отношения свойства.
  • Добавьте в базу данных и в язык запросов сведения о роде занятий и о месте жительства людей.
  • База данных "Династия" (Романовых, Рюриковичей или др.).
  • База данных "География России", описывающая взаимоотношения между объектами некоторой области.
  • База данных "Биология", описывающая взаимоотношения между растениями некоторого семейства.
  • База данных "Естественные языки", описывающая взаимоотношения между языками в некоторой группе языков.
  • База данных "Языки программирования", описывающая взаимоотношения между языками в некоторой парадигме.
  • Вернуться к учебному плану