Настоящая глава посвящена созданию базы данных "Родственные отношения". База может изменяться во время исполнения программы, с помощью добавления и удаления фактов, в процессе диалога с пользователем. Рассматривается язык запросов к базе данных, близкий к естественному языку. Вычисляются ответы на запросы.
В данной главе создается консольный проект в объектном стиле.
Язык 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].
Данный параграф посвящен созданию базы данных (как обычно, вначале следует создать консольный проект).
Для реализации доступа к сведениям, хранящимся в базе данных, создается класс 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().
В настоящем параграфе определяются основные родственные отношения.
Создается класс 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, соответственно, если конкретизированной.
В настоящем параграфе рассматривается реализация общения пользователя с компьютером.
Создается модуль 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.10–12.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.13–12.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.16–12.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.19–12.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.22–12.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.25–12.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.
Настоящий параграф посвящен разработке языка запросов к базе данных, близкого к естественному языку. В запросах представлены операции объединения, пересечения и разности множеств (найти сыновей Петра и дочерей мужчин), композиции отношений (найти сыновей предков Петра) и отрицания (найти сыновей Петра, но не сыновей Марии). Объединение представляют союзы "и" и "или", пересечение — знак запятой "," (поэтому его нельзя использовать просто как знак пунктуации), а отрицание — частица "не". Все запросы формулируются через набор основных отношений, которыми являются бинарные отношения "родитель", "ребенок", "отец", "мать", "муж", "жена", "сын", "дочь", "сестра", "брат", "предок" и "потомок", а также унарные отношения "мужчина" и "женщина". Например, найти тетушек Петра можно с помощью запроса "найти сестер родителей Петра". Без аргумента в запросе могут быть только унарные отношения. Например, нельзя попросить "найти отцов", но можно "найти отцов мужчин и отцов женщин".
Язык запросов удовлетворяет следующей грамматике:
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.28–12.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 проверяет, является ли заданная подстрока префиксом строки и возвращает остаток строки.
В настоящем параграфе реализуется процедура поиска ответов на вопросы. Все вычисления проводятся на множестве идентификаторов. Результом вычисления запроса является список идентификаторов. Из списков удаляются повторяющиеся элементы. К спискам применяются операции объединения, пересечения и разности.
Для реализации вычислений создается класс calculation с интерфейсом calculation (листинги 12.31–12.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].
Данный параграф посвящен созданию базы данных (как обычно, вначале следует создать консольный проект).
Для реализации доступа к сведениям, хранящимся в базе данных, создается класс 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().
В настоящем параграфе определяются основные родственные отношения.
Создается класс 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, соответственно, если конкретизированной.
В настоящем параграфе рассматривается реализация общения пользователя с компьютером.
Создается модуль 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.10–12.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.13–12.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.16–12.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.19–12.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.22–12.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.25–12.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.
Настоящий параграф посвящен разработке языка запросов к базе данных, близкого к естественному языку. В запросах представлены операции объединения, пересечения и разности множеств (найти сыновей Петра и дочерей мужчин), композиции отношений (найти сыновей предков Петра) и отрицания (найти сыновей Петра, но не сыновей Марии). Объединение представляют союзы "и" и "или", пересечение — знак запятой "," (поэтому его нельзя использовать просто как знак пунктуации), а отрицание — частица "не". Все запросы формулируются через набор основных отношений, которыми являются бинарные отношения "родитель", "ребенок", "отец", "мать", "муж", "жена", "сын", "дочь", "сестра", "брат", "предок" и "потомок", а также унарные отношения "мужчина" и "женщина". Например, найти тетушек Петра можно с помощью запроса "найти сестер родителей Петра". Без аргумента в запросе могут быть только унарные отношения. Например, нельзя попросить "найти отцов", но можно "найти отцов мужчин и отцов женщин".
Язык запросов удовлетворяет следующей грамматике:
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.28–12.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 проверяет, является ли заданная подстрока префиксом строки и возвращает остаток строки.
В настоящем параграфе реализуется процедура поиска ответов на вопросы. Все вычисления проводятся на множестве идентификаторов. Результом вычисления запроса является список идентификаторов. Из списков удаляются повторяющиеся элементы. К спискам применяются операции объединения, пересечения и разности.
Для реализации вычислений создается класс calculation с интерфейсом calculation (листинги 12.31–12.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 (см. ниже) требуется создать базу данных, а также придумать и реализовать язык запросов к ней, близкий к естественному языку.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.