Практическая информатика

Рекурсивные процедуры


Рекурсия в большинстве языков программирования - это такой способ организации обработки данных, при котором программа (процедура) вызывает сама себя непосредственно, либо с помощью другой программы (процедуры).

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


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

На примере уже имеющейся у нас базы данных объясним преимущества использования рекурсии и особенности рекурсивных правил. Пусть имеются следующие факты:

больше(слон, лошадь). больше(лошадь, осел). больше(осел, собака). больше(осел, обезьяна).

Выполним запрос к базе данных

?- больше(осел, собака). Yes

Цель больше(осел, собака) была достигнута потому, что этот факт был сообщен Прологу при загрузке базы. Теперь проверим, больше ли обезьяна слона?

Нет, не больше. Мы получили такой ответ, какой и ожидали: соответствующий запрос, а именно больше(обезьяна, слон) не подтвердился. Но, что случится, если мы зададим вопрос по-другому?

?- больше(слон, обезьяна). No

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

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

больше(слон, обезьяна).

Для нашего маленького примера это означает добавление еще 5 фактов. Однако гораздо лучшим решением будет добавление в программу нового отношения, которое мы назовем больше_2. Животное X больше, чем животное Y, если это определено как факт (первое правило) или существует животное Z, для которого определен факт, что животное X больше, чем животное Z и может быть показано, что животное Z больше, чем животное Y (второе правило). На Прологе это запишется так:

больше_2(X, Y) :- больше(X, Y). больше_2(X, Y) :- больше(X, Z), больше(Z, Y).

Если в цепочке участвуют не три, а большее число объектов, то придется добавить новые правила:

больше_2(X, Y) :- больше(X, Z1), больше(Z1, Z2), больше(Z2, Y). больше_2(X, Y) :- больше(X, Z1), больше(Z1, Z2), больше(Z2, Z3), больше(Z3, Y). ...

Эта программа длинна и работать будет далеко не всегда. Она сможет просматривать базу данных только до определенной глубины, задаваемой максимальным количеством подцелей в правилах.

Поэтому воспользуемся более корректной и элегантной формулировкой. Ключевая идея здесь - определить отношение больше_2 с помощью его самого. Теперь второе (и последнее!) правило выглядит так:

больше_2(X, Y) :- больше(X, Z), больше_2(Z, Y).

Таким образом, итоговая программа будет иметь вид

больше_2(X, Y) :- больше(X, Y). больше_2(X, Y) :- больше(X, Z), больше_2(Z, Y).

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

ERROR: Out of local stack

Если теперь в запросе использовать предикат больше_2 вместо больше, то программа будет работать так, как и предполагалось:

?- больше_2(слон, обезьяна). Yes

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


Это происходит при помощи сравнения запроса с началом правила больше_2(X, Y) (т. е. с его головой). После этого двум переменным присваиваются значения: X = слон и Y = обезьяна.

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

В данном случае Пролог не может найти в базе данных факта больше(слон, обезьяна) и переходит к рассмотрению второго правила. Оно гласит, что для того, чтобы получить ответ на вопрос больше_2(X,Y) (с фиксированными значениями переменных, то есть больше_2(слон, обезьяна)), Пролог должен ответить на два подвопроса больше(X, Z) и больше_2(Z, Y), опять же с соответствующими значениями переменных. Процесс просмотра базы знаний с самого начала повторяется до тех пор, пока факты, составляющие цепочку между слон и обезьяна, не будут найдены, а запрос успешно обработан.

Любая рекурсивная процедура должна включать по крайней мере по одной из ниже перечисленных компонент.

  1. Нерекурсивную фразу, определяющую правило, применяемое в момент прекращения рекурсии.
  2. Рекурсивное правило, первая подцель которого вырабатывает новые значения аргументов, а вторая - рекурсивная подцель- использует эти значения.


Задание

Дана база данных "Родители", в которой предикат родитель(коля, андрей) означает, что Коля является родителем Андрея:

родитель(коля, андрей). родитель(андрей, саша). родитель(виктор, федор). родитель(виктор, петр). родитель(петр, елена).

Используя рекурсию, определите отношение предок/2 через отношение родитель/2. Будем говорить, что некоторый X является отдаленным предком некоторого Y, если между X и Y существует цепочка людей, связанных между собой отношением родитель - ребенок.


Содержание раздела