Скачиваний:
53
Добавлен:
01.05.2014
Размер:
565.76 Кб
Скачать

7.5. Рекурсивное программирование в чистом Прологе

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

Программы 3.12 и 3.15, записанные для отношений member и append, являются корректными программами на Прологе. Так как они являются минимальными рекурсивными программами, то не возникает проблемы, связанной с порядком целей. Преимущества выбранного в программах порядка предложений уже обсуждались в данной главе. Вопрос о завершении программ рассматривался в разд. 7.2.

Программа 3.19, задающая отношение select, аналогична программе, задающей отношение member.

select(X,[X | Xs],Xs).

select(X,[Y | Xs],[Y | Ys])  select(X,Xs,Ys).

Анализ программы 3.19 похож на анализ программы 3.12. Порядок целей не рассматривается, так как программа - минимальная рекурсивная. Порядок предложений выбран таким, чтобы отражать естественный порядок решения вопросов типа select(X,[a,b,c],Xs) например {X = a.Xs = [b,c]},{X = b, Xs = [a,c]},{X = с, Xs = [a,b]}. Первое решение получено в результате удаления первого элемента и т.д. Программа завершается всегда, кроме тех случаев, когда второй и третий аргументы являются неполными списками.

Новая версия отношения select возникает при добавлении проверки Х Y в рекурсивное предложение. Как и раньше, мы предполагаем, что отношение  определено лишь для основных аргументов. Эта версия описана в программе 7.4. задающей отношение select_first(X.Xs,Ys). Программы 3.12 и 7,2. задающие отношения member и member_check, имеют одинаковые значения. В отличие от

select_.first(X,Xs.Ys)

список Ys получается из списка Xs удалением первого вхождения элемента X

select_first (X, [X | Xs],Xs).

se]est_first(X,[Y | Ys],[Y | Zs])

X  Y.select_first (X,Ys,Zs).

Программа 7.4. Выбор первого вхождения элемента в список.

значения программ 7.4 и 3.19 не совпадают. Цель select (a,[a,b,a,c],[a,b,c]) принадлежит значению программы select, а цель select_first(a,[a,b.a,c],[a,b,c]) не принадлежит значению программы select_first.

Теперь мы рассмотрим программу 3.20, задающую отношение permutation. Порядок целей в данной программе, как и порядок целей в программе append наиболее точно соответствует способу использования:

permutation(Xs,[X | Ys])  select(X,Xs,Zs), permutation(Zs, Ys).

permutation([ ],[ ]).

Порядок целей и проблема завершения программы permutation тесно связаны между собой. Вычисление целей permutation, имеющей в качестве первого аргумента полный список, неизбежно остановится. Вопрос вызывает процедуру select с полным списком в качестве второго аргумента, которая завершается получением полного списка в качестве третьего аргумента. Таким образом, перед рекурсивным вызовом цели permutation будет построен полный список, соответствующий первому аргументу. Если же первый аргумент - неполный список, то вычисление не завершится, поскольку произойдет обращение к процедуре select, которое приведет к бесконечному вычислению. Если переставить цели в рекурсивном правиле программы permutation, при анализе вопроса о завершении решающим становится значение второго аргумента. Если это значение - неполный список, то вычисление не завершится.

Полезным предикатом, использующим отношение , является nonmember(X,Ys), выполненный, если Х не входит в список Ys в качестве элемента. Его декларативное определение очевидно: элемент не входит в список, если он отличается от головы списка и не входит в его хвост. Базисный факт- никакой элемент не входит в пустой список. Программа приведена в виде программы 7.5:

nonmember[X.Xs)-

X не является элементом Xs

nonmember(X,[Y|Ys])  Х  Y, nonmember(X,Ys).

nonmember(X, [ ]).

Программа 7.5. Отсутствие вхождений в список.

Так как в программе используется отношение , то применение предиката nonmember ограничено основными примерами. С интуитивной точки зрения это разумно. Существует сколь угодно много элементов, не входящих в данный список, и сколь угодно много списков, не содержащих данный элемент. Таким образом, поведение программы 7.5 при решении подобных вопросов нее слишком существенно.

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

Мы рассмотрим построение двух программ, связанных с отношением “подмножество”. Программа 7.6 определяет предикат, основанный на отношении member в программе 3.12; программа 7.7 определяет предикат, основанный на отношении select, в программе 3.19. В обеих программах рассматривается вхождение элементов первого списка во второй список.

members(Xs.Ys)-

каждый элемент списка Xs является элементом списка Ys.

members([X | Xs],Ys)  member (X,Ys), members (Xs,Ys).

members([ ],Ys).

Программа 7.6. Определение подмножества.

selects (Xs, Ys) -

список Xs является подмножеством списка Ys.

selects([X|Xs],Ys)  select (X.Ys,Ys1), selects(Xs,Ys1).

selects([ ],Ys).

select(X,Ys,Zs)  См. программу 3.19.

Программа 7.7. Определение подмножества.

Программа 7.6, определяющая предикат members(Xs,Ys), игнорирует неоднократные вхождения элементов в списки. Например, members([b.b], [a,b,c]) принадлежит значению программы. В первом списке имеются два вхождения элемента b, а во втором - только одно.

Программа 7.6 имеет ограниченную область завершения. Если или первый, или второй аргумент цели members- неполный список, то программа не завершится. Второй аргумент должен быть полным списком ввиду вызова процедуры member, a первый аргумент должен быть полным списком, так как он обеспечивает управление рекурсией. Поиск решения вопроса members(Xs,[1, 2,3]), в котором спрашивается о всех подмножествах заданного множества, не завершится. Так как в Xs допускаются многократные вхождения одного и того же элемента, то существует бесконечное число решений и, следовательно, вычисление не закончится.

Оба ограничения устранены в программе 7.7. Пересмотренное отношение называется selects(Xs.Ys). Цели, принадлежащие значению программы 7.7, содержат не больше вхождений элемента в первый список, чем во второй. Благодаря этому свойству программа 7.7 завершается всякий раз, когда второй аргумент является полным списком. Такие вопросы, как selects(Xs,[a,h,c]) имеют в качестве решений все подмножества данного множества.

Рассмотрим теперь иной пример: пословный перевод списка английских слов в список французских слов. Отношение задается в виде translate(Words,Mots), где Words - список английских слов, a Mots- список соответствующих французских слов. Программа 7.8 выполняет перевод. В ней предполагается наличие словаря, содержащего пары соответствующих английских и французских слов; реляционная схема словаря -dict(Word,Mоt). Перевод очень наивный, не учитывающий число, род, спряжение и тому подобное. Область применения программы состоит из вопросов вида translate([the.dog.classes,the.cat],X)?, решением является Х = ([le,chien,classes,le,chat]). Эта программа может быть использована разными способами. Можно переводить английские предложения на французский, французские - на английский, или можно проверить, являются ли два предложения правильным переводом одного в другое.

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

translate (Words. Mots)

Mots- список французских слов. являющихся переводом списка английских слов Words.

translate([Word | Words],[Mot | Mots]) 

dict(Word, Mot), translate(Words, Mots).

translate[ ],[ ]).

dict (the, le). dict (dog, chien).

dict(classes, classe). dict (cat, chat).

Программа 7.8. Пословный перевод.

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

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

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

Рассмотрим подробнее характерный случай-построение одного результирующего значения по некоторому данному входу. В качестве примеров используем программу append для соединения двух списков с целью получения третьего и программу 7.8 для перевода списка слов с английского на французский. Вычисление происходит рекурсивно. Исходный вызов сопоставляет результату неполный список [X | Xs]. Голова списка X получает значение в результате вызова процедуры, чаще всего в процессе унификации с заголовком предложения. Хвост Xs получает значение постепенно, в процессе рекурсивного обращения. Структура становится окончательно определенной при применении исходных фактов и завершении вычисления.

Рассмотрим присоединение списка [c,d] к списку [а,b], как это описано на рис. 4.3. Результат строится поэтапно в виде Ls = [a \ Zs],Zs = [b,Zs], и, наконец, применение исходного факта программы append дает результат Zs1 = [c,d]. Каждое рекурсивное обращение частично определяет начально неполный список. Заметим, что рекурсивные вызовы процедуры append не должны обращаться к уже вычисленному списку. Это-построение рекурсивных структур нисходящим методом, типичное для программирования на Прологе.

Построение рекурсивных структур данных нисходящим методом имеет одно ограничение. Построенные части глобальных структур данных не могут использоваться в дальнейших вычислениях. Проиллюстрируем это на примере программы, определяющей отношение по_doubles (XXs,Xs), выполненное, если список Xs содержит все элементы списка XXs без повторных вхождений элементов.

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

no doubles([X|Xs],..)

Пробелы заполняются рекурсивным обращением к процедуре no_doubles с входом Xs и результатом Ys и последующим присоединением Х к Ys. Если Х еще не вошел в результат, то Х следует добавить к результату и в правой части должен находиться терм [X | Ys]. Если Х уже включен в результат, то его добавлять не следует, поэтому надо использовать терм Ys. У нас нет простого способа выразить это утверждение. Невозможно установить, как устроена уже построенная часть результата.

В программе no_doubles использован иной подход к проблеме. Вместо того чтобы определять, входит ли элемент в уже построенную часть результата, мы можем определить, появится ли он в дальнейшем. Для каждого элемента Х проверяется, появится ли он еще раз в хвосте списка - в Xs. Если Х появится, то результатом будет Ys - результат рекурсивного обращения к процедуре no_doubles. Если Х не появится, то Х добавляется к результату рекурсивного обращения. Эта схема определения отношения no_doubles реализована в программе 7.9. В ней использована программа 7.5, определяющая отношение nonmember.

nо_doubles (Xs,Ys)

список Ys получен удалением всех повторных вхождений из списка Xs.

no_doubles([X | Xs],Ys) 

member(X,Xs), no_doubles(Xs,Ys).

no..doubles([X | Xs],[X | Ys])

nonmember(X,Xs),no_doubles(Xs,Ys).

nonmember(X,Xs)  См. программу 7.5.

Программа 7.9. Удаление повторов из списка.

Программа 7.9 может оказаться неудобной, так как список, не включающий повторные вхождения, может содержать элементы не в надлежащем порядке. Например, вопрос no_doubles([a.b.c.b]Xs)? имеет решение Xs = [а,с,b], в то время как решение Xs = [а.b] может быть предпочтительнее. Данное требование будет выполнено, если переделать программу. Как только мы находим какой-то элемент в списке, мы удаляем его из остатка списка. Это можно сделать, заменив в программе 7.9 два рекурсивных вызова на правило.

no_doubles([X | Xs],[X | Ys])  delete(X,Xs,Xs1), no_doubles(Xs1,Ys).

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

Альтернативной для построения структур является стратегия снизу вверх (восходящий метод). Программа 3.16b служит простым примером подобного построения структур данных. В этой программе обращаются списки:

reverse(Xs,Ys)  reverse(Xs,[ ],Ys).

reverse([X | Xs].Acc,Ys)  reverse(Xs,[X | Acc],Ys).

reverse([ ],Ys,Ys).

К аргументам отношения reverse/2 добавлен дополнительный аргумент, предназначенный для накопления в процессе вычисления значений обращенного списка. Данная процедура reverse строит результат не нисходящим, а восходящим методом. Протокол, приведенный на рис. 7.3, описывает решение цели reverse([a,b,c],Xs), последовательные значения среднего аргумента при обращении к процедуре reverse/3-[ ],[a],[b,а], [с,b,а\. Эта последовательность соответствует создаваемой структуре.

reverse([a,b,c] ,Xs)

reverse([a,b,c],[ ],Xs)

reverse([b,c],[a],Xs)

reverse([c],[b,a],Xs)

reverse([ ],[c,b,a],Xs) Xs=[c,b,a]

true

Рис. 7.3. Протокол вычисления reverse.

При построении восходящим методом разрешен доступ в процессе вычисления к частичным результатам. Рассмотрим отношение nd_reverse(Xs,Ys), объединяющее в себе свойства отношений по_doubles и reverse. Смысл отношения nd_reverse состоит в том, что Ys - список элементов из Xs, расположенных в обратном порядке и без повторов. Так же как и в случае отношения reverse, отношение nd_reverse использует предикат nd_reverse/3 с дополнительным аргументом, предназначенным для построения результата восходящим методом. Этот аргумент и используется при проверке вхождения отдельных элементов в отличие от программы 7.6, в которой при проверке используется хвост списка. Искомой программой является программа 7.10.

nd_reverse (Xs, Ys)

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

nd_reverse(Xs,Ys) nd_reverse(Xs,[ ],Ys). nd_reverse([X | Xs],Revs,Ys) 

member(X, Revs), nd_reverse(Xs, Revs,Ys). nd_reverse([X | Xs],Revs,Ys) 

nonmember(X,Revs), nd_reverse(Xs,[X | Revs],Ys)

nd_reverse([ ],Ys,Ys),

nonmember(X,Xs)  См. программу 7.5.

Программа 7.10. Обращение без повторов.

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

Техника добавления “накопителя” к программе допускает обобщения. Мы используем эту технику в следующей главе, рассматривая арифметические программы на Прологе. Накопители можно также рассматривать как специальный случай неполных структур данных, как будет показано в гл. 15.

Упражнение к разд. 7.5

1.Перепишите программу 7.9, задающую отношение no_doubles, используя конструкцию снизу вверх.

Соседние файлы в папке 1-13