logo
Языки программирования

17.2. Унификация

Хорновский клоз (Нот clause) — это формула, в которой с помощью конъюнк­ции («и»)элементарных формул выводится одиночная элементарная формула:

(s=>t)<=(t = tl||t2)^(S=>tl)

Логическое программирование основано на том наблюдении, что, ограничи­вая формулы хорновскими клозами, мы получаем правильное соотношение между выразительностью и эффективностью вывода. Такие факты, как t => t, являются выводами, которые ниоткуда не следуют, т. е. они всегда истинны. Вывод также называется головой формулы, потому при записи в инверсной форме оно появляется в формуле первым.

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

"wof" => "Hello world"?

Машина вывода пытается сопоставить цель и вывод формулы. В данном слу­чае соответствие устанавливается сразу же: "wor" соответствует переменной s, a "Hello world" — переменной t. Это определяет подстановку выражений (в данном случае констант) для переменных; подстановка применяется ко всем переменным в формуле:

"wor" с "Hello world" c= ("Hello world" = tl || t2) л ("wor" с tl)

Теперь мы должны показать, что:

("Hello world" = t1|| t2) л ("wor" с tl)

является истинным, и это ведет к новому соответствию образцов, а именно попытке установить соответствие "Hello world" с tl || t2. Здесь, конечно, может быть много соответствий, что приведет к поиску. Например, машина вывода может допускать, чтобы tl указывало на "Не", a t2 указывало на "Но world"; эти подстановки затем проводятся во всем вычислении.

Знак «: — » обозначает импликацию, а переменные должны начинаться с про­писных букв. Когда задана цель:

?- substring ("wor", "Hello world").

вычисление пытается унифицировать ее с головой формулы; если это удается сделать, цель заменяется последовательностью элементарных формул (также называемых целями):

?- concat ("Hello world", T1,12), substring ("wor", T1).

Цель, которая получается в результате, может состоять из боле? чем одной эле­ментарной формулы; машина вывода должна теперь выбрать одну из них, что­бы продолжить поиск решения. По правилу вычисления языка Prolog машина вывода всегда выбирает крайнюю левую элементарную формулу. В данном при­мере правило вычисления требует, чтобы concat было выбрано перед рекур­сивным вызовом substring.

Головы нескольких формул могут соответствовать выбранной элементар­ной формуле, и машина вывода должна выбрать одну из них, чтобы попытать­ся сделать унификацию. Правило поиска в языке Prolog определяет, что фор­мулы выбираются в том порядке, в котором они появляются в тексте програм­мы. При попытке установить соответствие целевой формулы с формулами процедуры substring правило поиска требует, чтобы сначала была выбрана ис­тинная substring (Т,Т), затем вторая формула с substring (S, T1), и, только если она не выполняется, третья формула с substring (S, T2). ,

Основанием для этих, по-видимому, произвольных требований, послужи­ло то, что они дают возможность реализовать язык Prolog на стековой архи­тектуре точно так же, как языки С и Ada, и сделать большинство вычислений в языке Prolog столь же эффективными, как и в процедурных языках. Вычис­ление выполняется перебором с откатами (backtracking). В приведенном выше примере:

?- concat ("Hello world", Т1, Т2), substring ("wor", T1).

предположим, что вычисление выбрало для concat подстановку

["Н" -»tl, "ello world" -> t2]

Теперь делается попытка доказать substring ("wor", "H"), которая, очевидно, не выполняется. Вычисление делает откат и пытается найти другое доказа­тельство concat с другой подстановкой. Все данные, необходимые для вычис­ления substring ("wor", "Н"), можно отбросить после отката. Таким образом, правило вычисления в языке Prolog естественно и эффективно реализуется на стеке.

Чтобы еще улучшить эффективность программ, написанных на языке Prolog, в язык включена возможность, названная обрезанием (cut обозначается «!»), которая позволяет задать указание машине вывода воздержаться от по­иска части пространства возможных решений. Именно программист должен гарантировать, что никакие возможные решения не «обрезаны». Например, предположим, что мы пытаемся проанализировать арифметическое выраже­ние, которое определено как два терма, разделенных знаком операции:

expression (T1, OP, T2):- term (T1), operator (OP), !, term (T2).

operator ('+').

operator ('-').

operator ('*').

operator ('/').

и что цель — expression (n, '+', 27). Очевидно, что и п и 27 являются термами, а '+' — одним из операторов, поэтому цель выполняется. Если, однако, в качестве цели задать expression (n,'+', '>'), то вычисление при отсутствии об­резания продолжится следующим образом:

n — терм

'+' соответствует operator ('+')

'>' —нетерм

'+' не соответствует operator('-')

'+' не соответствует operator ('*')

'+' не соответствует operator ('/')

Машина вывода делает откат и пытается доказать operator (OP) другими спосо­бами в надежде, что другое соответствие позволит также доказать term (T2). Ко­нечно, программист знает, что это безнадежно: обрезание приводит к тому, что вся формула для expression дает неуспех, если неуспех происходит после того, как будет пройдено обрезание. Конечно, обрезание уводит язык Prolog еще дальше от идеального декларативного логического программирования, но обрезание активно используют на практике для улучшения эффективности программы.

Нелогические формулы

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

Другая область, в которой язык Prolog отходит от чистого логического про­граммирования, — численные вычисления. Конечно, в логике можно опреде­лить сложение; фактически, это единственный способ определить сложение строго:

N + 0 = N

N + s (М) = s (К) <= N + М = К

О — это числовой ноль, a s(N) — выражение для числа, следующего за N, так, например, s(s(s(0))) — выражение для числа 3. Формулы определяют «+», ис­пользуя два правила: 1) число плюс ноль — это само число, и 2) N плюс следующее за М — это следующее за N + М. Очевидно, было бы чрезвычай­но утомительно писать и неэффективно выполнять логическую версию для 555 + 777.

Prolog включает элементарную формулу:

Var is Expression

Вычисляется значение Expression, и создается новая переменная Var с этим значением. Обратите внимание, что это не присваивание; переменной нельзя присвоить значение еще раз, ее только можно будет использовать позднее как аргумент в какой-нибудь элементарной формуле.

Вычисление выражения и присваивание вновь созданной переменной можно использовать для моделирования циклов:

loop (0).

loop (N) :-

proc,

N1 isN-1,

loop(N1).

Следующая цель выполнит proc десять раз:

?-loop (10).

Аргумент является переменной, которая используется как индекс. Первая формула — базовый случай рекурсии: когда индекс равен нулю, больше ниче­го делать не нужно. В противном случае выполняется процедура proc, созда­ется новая переменная N1 со значениями N-1, которая используется как аргу­мент для рекурсивного вызова loop. Унификация создает новую переменную для каждого использования второй формулы loop. Нельзя сказать, что это слишком неэффективно, потому что это можно выполнить в стеке. К тому же многие компиляторы Prolog могут делать оптимизацию хвостовой рекурсии, т. е. заменять рекурсию обычной итерацией, если рекурсивный вызов являет­ся последним оператором в процедуре.

Причина того, что использование is — нелогическое, состоит в том, что оно не симметрично, т. е. вы не можете написать:

28 is V1 * V2

или даже:

28 is V1*7

где V1 и V2 — переменные, которые еще не получили своих значений. Для это­го потребовалось бы знать семантику арифметики (как разлагать на множите ли и делить целые числа), в то время как унификация устанавливает только синтаксическое соответствие термов.

База данных на языке Prolog

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

customer(1, "Jonathan"). /* клиент(Идент_клиента, Имя) */

customer(2, "Marilyn"),

customer^, "Robert").

salesperson 101, "Sharon"). /* продавец(Идент_продавца, Имя) */

salesperson 102, "Betty").

salesperson 103, "Martin").

order(103, 3, "Jaguar"). /*заказ(Идент_продавца,

order(101, 1, "Volvo"). Идент_клиента, товар)*/

order(102, 2, "Volvo").

order(103, 1, "Buick").

Обычные цели языка Prolog могут интерпретироваться как запросы к базе данных. Например:

?- salesperson(SalesJD, "Sharon"), /* ID Шэрон */

order(SalesJD, CustJD, "Volvo"), /* Заказ Volvo */

customer(CustJD, Name). /* Клиент заказа */

означает следующее: «Кому Шэрон продала Volvo?». Если запрос успешный, переменная Name получит значение имени одного из клиентов. В противном случае мы можем заключить, что Шэрон никому Volvo не продавала.

Сложные запросы базы данных становятся простыми целями в языке Prolog. Например: «Есть ли клиент, которому продавали автомобиль и Шэ­рон, и Мартин?»:

?- salesperson(ID1,"Sharon"), /* ID Шэрон */

salesperson(ID2, "Martin"), /* ID Мартина */

order(ID1, CustJD, _), /* ID клиента Шэрон */

order(ID2, CustJD, _). /* ID клиента Мартина */

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

Является ли язык Prolog реальной альтернативой специализированному программному обеспечению баз данных? Реализация списков фактов вполне эффективна и может легко отвечать на запросы для таблиц из тысяч записей. Однако, если ваши таблицы содержат десятки тысяч записей, необходимы бо­лее сложные алгоритмы поиска. К тому же, если ваша база данных предназна­чена для непрограммистов, необходим соответствующий интерфейс пользо­вателя, и в этом случае ваша реализация языка Prolog может и не оказаться подходящим языком программирования.

Важно подчеркнуть, что «это не было сделано профессионалами», т. е. мы не вводили ни новый язык, ни понятия базы данных; это было всего лишь обычное программирование на языке Prolog. Любой программист может со­здавать небольшие базы данных, просто перечисляя факты, а затем в любой момент выдавать запросы.

Динамические базы данных

Если все наборы фактов, существуют с самого начала программы на языке Prolog, запросы совершенно декларативны: они только просят о заключении, основанном на ряде предположений (фактов). Однако язык Prolog включает нелогическое средство, с помощью- которого можно менять базу данных в процессе вывода. Элементарная формула assert(F) всегда истинна как логиче­ская формула, но в качестве побочного эффекта она добавляет факт F к базе данных; точно так же retract(F) удаляет факт F:

?- assert(order( 102, 2, "Peugeot")), /* Бетти продает автомобиль'*/

assert(order(103,1 , "BMW")), /* Мартин продает автомобиль */

assert(order(102, 1, "Toyota")), /* Бетти продает автомобиль*/

assert(order(102, 3, "Fiat")), /* Бетти продает автомобиль */

retract(salesperson(101, "Sharon")). /* Уволить Шэрон! */

С помощью изменений базы данных можно в языке Prolog смоделировать оператор присваивания. Предположим, что факт count(O) существует в про­грамме, тогда:

increment :-

N1 is N +1, /* Новая переменная с новым значением */

retract(count(N)), /* Стереть старое значение */

assert(count(N1)). /* Сохранить новое значение */

Ни одна из трех элементарных формул не является логической!

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

Сортировка в языке Prolog

В качестве примера соотношения между описательным и процедурным взгля­дами на логическую программу мы обсудим программы сортировки на языке Prolog. Мы ограничимся сортировкой списков целых чисел. Обозначения: [Head]Tail] является списком, первый элемент которого — Head, а остальные элементы образуют список Tail. [] обозначает пустой список.

Сортировка в логическом программировании вполне тривиальна, потому нам нужно только описать смысл того, что список L2 является отсортированной версией списка L1. Это означает, что L2 представляет собой перестановку (per­mutation) всех элементов L1 при условии, что элементы упорядочены (ordered):

sort(L1, L2):- permutation(L1, L2), ordered(L2).

где формулы в теле процедуры определены как:

permutation([], []).

permutation(L, [X | Tail]) :-

append(Left_Part, [X | Right_Part], L),

append(Left_Part, Right_Part, ShortJJst),

permutation(Short__List, Tail).

ordered([]).

ordered([Single]).

ordered([First, Second | Tail]) :-

First =< Second,

ordered([Second | Tail]).

Прочитаем их описательно:

• Пустой список является перестановкой пустого списка. Перестановка непустого списка является разделением списка на элемент X и две части Left_Part и Right_Part, так, что X добавляется в начало перестановки кон­катенации двух частей. Например:

permutation([7,2,9,3], [91Tail])

если Tail является перестановкой [7,2,3].

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

С точки зрения процедуры это не самая эффективная программа сорти­ровки; действительно, ее обычно называют медленной сортировкой! Она просто перебирает (генерирует) все перестановки списка чисел, пока не найдет отсортированный список. Однако также просто написать описательную вер­сию более эффективных алгоритмов сортировки типа сортировки вставкой, который мы рассмотрели на языке ML в предыдущей главе:

insertion_sort([], []).

insertion_sort([Head | Tail], List) :-

insertion_sort(Tail, Tail _1),

insert_element(Head, Tail_1, List).

insert_element(X, [], [X]).

insert_element(X, [Head | Tail], [X, Head | Tail]) :-

X=<Head.

insert_element(X, [Head Tail], [Head Tail_1]) :-

insert_element(X, Tail, Tail_1).

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

Типизация и «неуспех»

В языке Prolog нет статического контроля соответствия типов. К сожалению, реакция машины вывода языка Prolog на ошибки, связанные с типом переменных, может вызывать серьезные затруднения для программиста. Предположим, что мы пишем процедуру для вычисления длины списка:

length([], 0). /* Длина пустого списка равна 0 */

length([Head | Tail], N) : - /* Длина списка равна */

length(Tail, N1), /* длине Tail */

N is N1+1. /* плюс 1*/

и случайно вызываем ее с целочисленным значением вместо списка:

?- length(5, Len).

Это не запрещено, потому что вполне возможно, что определение length со­держит дополнительную нужную для отождествления формулу.

Машина вывода при вызове lenght в качестве реакции просто даст неуспех, что совсем не то, что вы ожидали. A length была вызвана внутри некоторой другой формулы р, и неуспех length приведет к.неуспеху р (чего вы также не ожидали), и так далее назад по цепочке вызовов. Результатом будут неуправляемые откаты, которые в конце концов приведут к неуспеху перво­начальной цели при отсутствии какой-либо очевидной причины. Поиск таких

ошибок — очень трудный процесс трассировки вызовов шаг за шагом, пока ошибка не будет диагностирована.

По этой причине некоторые диалекты языка Prolog типизированы и требу­ют, чтобы вы объявили, что аргумент является или целым числом, или спи­ском, или некоторым типом, определенным программистом. В типизирован­ном языке Prolog вышеупомянутый вызов был бы ошибкой компиляции. В таких диалектах мы снова встречаем привычный компромисс: обнаружение ошибок во время компиляции за счет меньшей гибкости.

Yandex.RTB R-A-252273-3
Yandex.RTB R-A-252273-4