Министерство образования и науки российской федерации федеральное агентство по образованию санктпетербургский государственный университет
Скачать 1.41 Mb.
|
Таким образом, программа на Прологе состоит из предикатов. Программа на Прологе и база знаний - синонимы. Цель формулируется также в виде предикатов. Выполнение программы на Прологе – это резолюция цели. 1.3. Как работает интерпретатор Пролога? Процесс нахождения решения в Прологе заключается в сопоставлении предиката цели с предикатами базы знаний. Этот процесс называется унификацией. Пусть Пролог-системе предъявлена цель из предыдущего подраздела (мы хотим найти, чьей внучкой является Кристина): parent(X, kristina), parent(Y, X). Пролог выделяет из нее первую подцель parent(X, kristina) и начинает сопоставлять ее с базой знаний (проводить унификацию). База знаний повторена ниже. parent(boris, alla). parent(bedros, filipp). parent(edmuntas, kristina). parent(alla, kristina). parent(kristina, denis). Вначале сопоставляются первый предикат и подцель: parent(boris, alla) и parent(X, kristina) Первый аргумент boris сопоставляется с переменной X. Следует иметь в виду, что в Прологе различаются состояния переменных free (свободные) и bound (связанные). Если обе переменные связаны, то при унификации происходит их сравнение. Если одна из них свободна, то происходит присвоение. Переприсвоение значений переменным не допускается. Это существенно отличает Пролог от прочих языков. Таким образом, переменной Х, которая пока является свободной, присваивается значение boris. После этого унифицируются вторые аргументы, alla и kristina. Поскольку это константы, и alla не равно kristina, то унификация предиката parent(boris,alla) и подцели parent(X, kristina) заканчивается неудачей (fail). Поскольку в базе знаний несколько экземпляров предиката parent, такой предикат называется неоднозначным (non-derministic). Если предикат один, то он называется однозначным (deterministic). В случае неоднозначного предиката после неудачи выполняется откат – переход к следующему экземпляру предиката. При этом отменяется также присвоение значение переменным, если таковое имело место. Затем выполняется унификация предикатов parent(bedros, filipp) и parent(X, kristina) Очевидно, что результат унификации будет тот же, неудача (fail). При откате на следующий предикат parent(edmuntas,kristina) картина будет иная: X 10 присвоится значение edmuntas, а сопоставление вторых аргументов будет также успешным, так как kristina = kristina. Таким образом, первая подцель окажется выполненной. Пролог запоминает, какой экземпляр предиката сработал и устанавливает на следующий предикат указатель отката: parent(boris, alla). parent(bedros, filipp). parent(edmuntas, kristina). > parent(alla, kristina). parent(kristina, denis). после чего перейдет ко второй подцели parent(Y, X), где X = edmuntas, т.е. Пролог ставит себе такую подцель: parent(Y, edmuntas). В поисках родителя Эдмунтаса Пролог снова начинает унифицировать этот предикат с начала базы знаний, начиная с parent(boris, alla). Нетрудно видеть, что на этот раз перебор всех предикатов закончится неудачей, т.е. подцель parent(Y, edmuntas) не дала положительного решения. В этом случае Пролог откатывается к предыдущей подцели (продвигается назад по списку подцелей) и пытается найти альтернативное решение для parent(X, kristina).При этом X опять становится свободной переменной, а Пролог возвращается к точке отката: parent(boris, alla). parent(bedros, filipp). parent(edmuntas, kristina). > parent(alla, kristina). parent(kristina, denis). то есть к предикату parent(alla, kristina),сопоставляя его с подцельюparent(X, kristina). Теперь X присваивается значение alla, указатель отката устанавливается на предикат parent(kristina, denis) и опять выполняется переход к следующей подцели parent(Y, X), где X = alla. Пролог снова начинает унификацию подцели parent(Y, alla)c базой знаний, начиная с первого предиката. В первом предикате происходит унификация константы boris и свободной переменной Y. Происходит присвоение Y=boris, затем сопоставляются вторые аргументы. Так как alla = alla, сопоставление завершается успешно. Таким образом, решение найдено: Кристина является внучкой Бориса. Заметим, что неудача, которая постигла нас в поисках деда Кристины по отцовской линии, связана только с неполнотой базы знаний. Итак, интерпретатор Пролога автоматически выполняет поиск решения. Механизм поиска реализован с помощью отката после неудачи. Откат происходит на следующий экземпляр неоднозначного предиката. Выполнение программы на Прологе (резолюция цели) заключается в унификации цели с базой знаний. 11 1.4. Факты и правила в Прологе Описанный выше запрос, устанавливающий отношение типа "прародитель-внук" может потребоваться в дальнейшем неоднократно. В этой связи его целесообразно запомнить для дальнейшего использования в других запросах. В базе знаний Пролога можно хранить не только факты, но и правила, т.е. условные отношения. Отношение типа "прародитель-внук" может быть записано следующим образом: grandparent(X,Y) if parent(X,Z), parent(Z,Y). Читать это нужно следующим образом: X является прародителем Y, если X является родителем Z и Z является родителем Y. Предикат grandparent(X,Y) называется заголовком правила, а выражение справа от if – телом правила. Примечание: Синонимом связки "if" в правиле являются символы ":-". Таким образом, как и в базах данных, в базе знаний Пролога в виде фактов мы храним первичные знания, а производные от них записываем в виде правил, к которым обращаемся так же, как и к фактам. Факт – это то, что известно. Правило – это способ порождения новых фактов на основе имеющихся. Для родственных отношений мы можем установить множество правил, избавляясь от необходимости вводить дополнительные факты, например, кто кому приходится братом, племянником и т.д. Правило, определяющее отношение брат (сестра): sibling(X,Y) :- parent(Z,X), parent(Z,Y), X<>Y. Предикат сравнения X<>Y нужен для разрешения коллизии типа "сын моего отца, но мне не брат". Правило, определяющее отношение типа дядя, выглядит следующим образом: uncle(X,Y) :- parent(Z,Y), sibling(X,Z). Когда в ходе резолюции цели Пролог встречает не факт, а правило, то вначале унифицирует заголовок правила, т.е. сравнивает связанные переменные и присваивает значения свободным переменным. В случае успешной унификации аргументов Пролог подставляет значения аргументов из заголовка в первый предикат в теле правила и ставит этот предикат себе в качестве подцели, которую начинает унифицировать с базой знаний. В случае успешной резолюции данной подцели Пролог переходит к следующему условию правила. Если унификация этого предиката условия приводит к неудаче, то Пролог выполняет откат к предыдущему условию правила. Этот откат происходит только в том случае, если этот предыдущий предикат является неоднозначным. Поясним это на примере. Зададимся целью найти, кто является прародителем Кристины: grandparent(Who, kristina). Получив такую цель, Пролог начинает унифицировать ее с правилом: grandparent(X,Y) :- parent(X, Z), parent(Z,Y). Переменная Who в предикате цели является свободной переменной и ее унификация с переменной X в заголовке правила будет успешной всегда. Следует заметить, что в Прологе все 12 переменные являются локальными, т.е. существует только внутри правила. Мы могли бы использовать X вместо Who, и это были бы разные переменные, которые бы унифицировались точно так же. При необходимости создания глобальных переменных используют динамические факты, которые создаются предикатом assert и уничтожаются предикатом retract или retractall. Далее унифицируются константа kristina c переменной Y. Поскольку переменные в заголовке правила всегда сначала являются свободными, выполняется присвоение: Y = kristina. Поскольку унификация заголовка правила прошла успешно, Пролог углубляется в тело правила и ставит себе в качестве подцели первый предикат тела правила, подставляя переменные, если они связанные: parent(X, Z). Переменные X и Z являются свободными, поэтому успешной будет унификация данной подцели с первым же предикатом parent из базы знаний: X = boris, Z = alla После этого Пролог переходит ко второму предикату в правиле, подставляя значение X и Y: parent(alla, kristina). Резолюция данной подцели дает истину, а значения переменных, присвоенные в ходе унификации, возвращаются Прологом: Who = X = boris. Таким образом, в ходе резолюции основной цели Пролог самостоятельно ставит себе подцели, руководствуясь правилами, находящимися в базе знаний. Рассмотрим другой пример: grandparent(Who, denis). Аналогично предыдущему примеру Пролог унифицирует заголовок правила grandparent(X,Y) и присваивает значение Y = denis. Углубляясь в тело правила, Пролог формирует подцель parent(X, Z). Данная подцель возвращает, как и в предыдущем примере, X = boris, Z = alla Пролог переходит ко второму предикату правила, подставляя в parent(Z,Y) значения переменных: parent(alla, denis). Пытаясь унифицировать данную подцель, Пролог сопоставляет переменные (alla, denis) с первым экземпляром предиката parent, терпит неудачу, откатывается к следующему экземпляру и так далее. Поскольку факта parent(alla, denis) в базе знаний нет, резолюция данной подцели оказывается неудачной. Следовательно, унификация первого предиката правила значениями X = boris, Z = alla является неверной. Поэтому Пролог выполняет откат к предыдущему условию правила и пытается найти другое решение для подцели parent(X, Z). При этом отменяется присвоение переменных (X = boris, Z = alla). Переменные X и Z вновь становятся свободными. Заметим, что откат здесь возможен только на неоднозначный предикат. Если в цепочке предикатов 13 внутри правила встречаются как однозначные, так и неоднозначные предикаты, то откат после неудачи выполняется на ближайший неоднозначный предикат. При первой унификации данного предиката Пролог установил указатель отката на следующий экземпляр факта parent: parent(boris, alla). > parent(bedros, filipp). parent(edmuntas, kristina). parent(alla, kristina). parent(kristina, denis). При откате Пролог приступает к унификации данного факта и устанавливает указатель отката на третий экземпляр: parent(boris, alla). parent(bedros, filipp). > parent(edmuntas, kristina). parent(alla, kristina). parent(kristina, denis). После унификации второго предиката parent с подцелью parent(X, Z) Пролог присвоит значения переменным: X = bedros, Z = filipp и снова переходит ко второму предикату parent(Z,Y): parent(bedros,denis). Очевидно, резолюция и этой подцели завершается неудачей. Пролог снова откатывается к предыдущему предикату правила и к третьему экземпляру факта parent. Успешой окажется только унификация четвертого факта: parent(alla, kristina), в результате чего мы получим Who = X = alla. Так работает интерпретатор Пролога в случае наличия правил в базе знаний. Таким образом, Факт – знания, основанные на константах (неизменяемые знания). Правила – знания, которые выводятся на основании фактов. Набор фактов и правил не содержит в себе алгоритма. Правила и факты существуют независимо друг от друга. Объединение правил для вывода результата происходит в ходе резолюции цели. Переменные в заголовке правила существуют только внутри данного правила. При откате внутри правила происходит переход к предыдущему неоднозначному предикату в правиле. 1.5. Рекурсии в языке Prolog Если нас интересует, является ли X предком Y, то мы должны последовательно ставить цели: parent(X,Y). 14 grandparent(X,Y). grandgrandparent(X,Y). grandgrandgrandparent(X,Y). и так далее, где grandgrandparent(X,Y) :- parent(X,Z), grandparent(Z,Y). grandgrandgrandparent(X,Y) :- parent(X,Z), grandgrandparent(Z,Y). Вместо этого Пролог позволяет записать данное правило следующим образом: predecessor(X,Y) :- parent(X,Y). predecessor(X,Y) :- parent(X,Z), predecessor(Z,Y). Здесь имеет место рекурсивный вызов предиката predecessor. Рекурсия в Прологе является мощным средством, позволяющим строить очень компактные и эффективные программы. Рассмотрим использование рекурсии на примере вычисления факториала. n! = n·(n-1) ·(n-2) ·… ·1 Рекурсивное определение факториала: 0! = 1; n! = n·(n-1)! Программа на прологе, реализующая вычисление факториала будет выглядеть следующим образом: f(0,1). f(N,F) :- N1=N-1, f(N1,F1), F=F1·N. Обратите внимание, что программа, в сущности, состоит не более, чем из рекурсивного математического описания функция факториала, приведенного выше. Запустим программу в режиме трассировки и зададим цель f(3, X). Пролог начинает сопоставлять предикат цели с базой знаний (CALL означает вызов предиката, RETURN – завершение работы предиката, FAIL – неудачу, REDO – откат): CALL f (3, X) цель сопоставляется с f(0,1) FAIL неудача, т.к. 3 ≠ 0 REDO f (3, X) Происходит откат на следующий экземпляр f() N= 3, Входим в тело правила. Присваиваем N=3 N1= 2 Находим N1=2 f (2, X) Пролог ставит себе подцель CALL f (2, X) которая сопоставляется с f(0,1) FAIL неудача, т.к. 2 ≠ 0 REDO f (2, Х), Происходит откат на следующий экземпляр f() N= 2, Опять входим в тело правила. Присваиваем N=2 N1=1 Находим N1=1. Это уже другое N1 f (1, X) Пролог ставит себе подцель f (1, X) CALL f (1, X) которая сопоставляется с f(0,1) FAIL неудача, т.к. 1 ≠ 0 REDO f (1, X) Происходит откат на следующий экземпляр f() N=1 Опять входим в тело правила. Присваиваем N=1 15 N1= 0 Находим N1=0. f(0, X) Пролог ставит себе подцель f(0, X) CALL f(0, X) которая сопоставляется с f(0,1) RETURN X=1 Успешно. Возврат из нижнего уровня рекурсии F=1 Умножение 1 на 1 (факториал от 1) RETURN X=1 Возврат из следующего уровня рекурсии F=2 Умножение 2 на 1 (факториал от 2) RETURN X=2 Возврат из следующего уровня рекурсии F=6 Умножение 3 на 2 (факториал от 3) RETURN X=6 Возврат из программы Можно заметить, что логика работы программы вычисления факториала зависит от расположения в тексте предиката, определяющего выход из рекурсии. Унификация выполняется в порядке следования предикатов в тексте программы, и если предикат f(0,1) поставить в конце, выход из рекурсии будет невозможен. Таким образом, декларативность Пролога не является абсолютной для удобства его использования. Вариант программы, в котором предикаты могут располагаться в любом порядке, представлен ниже. f(N,F) :- N>0, N1=N-1, f(N1,F1), F=F1·N. f(0,1). Заметим также, что в данном рекурсивном предикате есть действия в теле правила после рекурсивного вызова. Это называют нарушением хвостовой рекурсии (tail recursion). Нарушение хвостовой рекурсии, называемое также нехвостовой рекурсией, требует запоминания всего окружения рекурсивного вызова (а не только результата), поэтому приводит к большим затратам памяти. Существуют приемы устранения нехвостовых рекурсий. Ниже приведен пример вычисления факториала без нехвостовой рекурсии. f(N1,N,F1,F) :- % если N1! = F1, то N! = F N2=N1+1, F2=F1*N2, % (N1+1)! = F2 f(N2,N,F2,F). % если N2! = F2, то N! = F f(N,N,F,F). % Условие выхода из рекурсии Цель для вычисления 3! выглядит следующим образом: f(0,3,1,F). Рекурсия в Прологе не всегда используется для выполнения многократно повторяющихся действий. Вспомним базу знаний «звездной» семьи, в которой есть предикат, описывающий супружеские отношения, в частности, spouse(filipp,alla). Супружеские отношения в отличие от родительских отношений, являются симметричными. Филипп является супругом Аллы, равно как и Алла является супругой Филиппа. При этом в предикате положение аргументов является фиксированным. Иными словами, если факт в базе знаний записан следующим образом: 16 spouse(filipp,alla). а предикат цели таким: spouse(alla,filipp). то результат будет отрицательным. Для того, чтобы показать Прологу, что это предикат является симметричным в отношении аргументов, мы можем применить правило: spouse(X,Y) :- spouse(Y,X). В качестве примера рассмотрим известную коллизию. В деревне жили две семьи: мать с дочерью и отец с сыном. Дадим им имена. Пусть мать и дочь зовут Мария и Даша, а отца и сына Олег и Сергей соответственно. Первые буквы имен подскажут нам, кто есть кто, иначе мы запутаемся. В базе знаний эти факты найдут свое отражение в следующем виде: parent(oleg, sergei). parent(maria, dasha). В силу превратностей судьбы Олег женился на Даше, и Мария вышла замуж за Сергея: spouse(oleg,dasha). spouse(sergei, maria). Для симметрии супружеских отношений введем правило: spouse(X, Y) :- spouse(Y, X). Нравы в деревне простые, поэтому жену отца положено называть мамой, а мужа матери – отцом. Правила для этого будут такими: parent(X,Y) :- spouse(X,Z ), parent(Z,Y). Попробуем найти внука Сергею. Ставим цель grandparent(sergei, Who). Пролог находит правило grandparent(X,Y) :- parent(X,Z), parent(Z,Y) и ставит себе подцель из первого предиката в теле этого правила: parent(sergei, Z). У Сергея детей нет, поэтому Пролог обращается к правилу parent(X,Y) :- spouse(X, Z), parent(Z,Y) и ставит себе цель spouse(sergei, Z). Пролог дает результат Z = maria. Второй предикат правила parent parent(maria,Y). Возвращается значение Y = dasha. То есть Сергей в качестве мужа Марии числится отцом Даши. Теперь Пролог переходит ко второму предикату в правиле grandfather: parent(dasha,Y). Даша также не имеет детей, поэтому Пролог обращается к правилу parent(X,Y) :- spouse(X,Z ), parent(Z,Y) и сначала пытается найти супруга Даше: spouse(dasha, Z). Результат: Z = oleg. Второй предикат этого правила parent(oleg,Y) даст Y = sergei. То есть Сергей приходится внуком самому себе! Созданная нами база знаний верно отражает данную коллизию. |