Декларативное выражение рекурсии…
Большинство программистов знакомы с рекурсией, по крайней мере, на каком-то уровне. В императивных языках рекурсия обычно не требуется для простых алгоритмов, которые могут использовать циклические операторы потока управления, определенные в языке. В чисто функциональных языках, где конструкции циклов недоступны, рекурсия необходима для выполнения любого алгоритма цикла. Как и в чисто функциональном языке, в Прологе нет циклических конструкций, доступных при написании программы, поэтому программирование на Прологе требует умения работать с рекурсией.
Если вы уже знакомы с рекурсией, а также с простым программированием на Прологе, вы можете пропустить разделы «учебник» и сразу перейти к разделу «Рекурсия в Прологе».
Основы рекурсии
Рекурсивная функция состоит из одного или нескольких базовых случаев и одного или нескольких рекурсивных случаев. Базовый случай — это ситуация, в которой проблема решаема напрямую. Если базовый случай не применяется, то рекурсивный случай создает одну или несколько меньших проблем того же типа, что и исходная проблема, для рекурсивного решения, а затем использует рекурсивное решение (решения) для формулировки решения исходной проблемы. Важно отметить, что рекурсивные случаи должны гарантировать, что подзадачи ближе к ситуации базового случая, иначе рекурсия не закончится.
В следующем псевдокоде показано рекурсивное решение двух задач. Один из них, суммирующий числа от 0 до n, возможно, более естественно выражается циклом в императивном языке. Тем не менее, это можно сделать рекурсивно в языках без циклических конструкций.
sum(n) if n is less than 0, then return 0 if n is 0 or 1, then return n return n + sum(n-1)
Другой, перемещение башни из n дисков от исходной привязки к целевой привязке с помощью запасной привязки, не так просто выразить без рекурсии.
move_tower(n, source, destination, spare)
if n is less than 1, then output “error”
if n is 1, output “move top disk on {source} to {destination}”
else
move_tower(n-1, source, spare, destination)
output “move top disk on {source} to {destination}”
move_toweri(n-1, spare, destination, source)
Для тех, кто не знаком с проблемой, в башне есть диски разного размера, за раз можно перемещать только один диск, и ни один диск нельзя поставить на меньший диск. Рекурсивное решение перемещает меньшую башню, уложенную поверх самого большого диска, на запасную привязку, используя привязку назначения в качестве запасной, затем перемещает самый большой диск непосредственно в точку назначения и, наконец, перемещает меньшую башню от запасной привязки к привязке назначения. , используя исходную привязку в качестве запасной.

Введение в Пролог
Пролог является декларативным языком. Программа на Прологе представлена запросом к базе данных, состоящим из утверждений о фактах, которые истинны сами по себе, и правил, указывающих, как доказать, что что-то истинно. Правила состоят из имени предиката и параметров с левой стороны и предложений с правой стороны, обычно разделенных запятыми, что означает «и», или точкой с запятой, что означает «или». Слова с заглавной буквы — это переменные, которые Пролог пытается унифицировать со значениями, которые сделают предикат истинным. Если непротиворечивая унификация невозможна, Пролог отступает, чтобы попробовать другое правило для предиката, или терпит неудачу, если были испробованы все способы.
В качестве простого примера программирования на Прологе рассмотрим рассуждения о классификации животных. Во-первых, у нас есть факты о конкретных животных, использующие описательные предикатные имена и символы для представления отдельных животных.
bear(b). elephant(e). frog(f). ostrich(o). salmon(s).
Теперь мы можем составить правила для некоторых предикатов: теплокровный, хладнокровный, млекопитающий, птица, рыба и земноводное. Обратите внимание, что это лишь небольшая часть общей программы классификации животных.
warm_blooded(A) :- mammal(A) ; bird(A). cold_blooded(A) :- fish(A) ; amphibian(A). mammal(A) :- bear(A) ; elephant(A). bird(A) :- ostrich(A). fish(A) :- salmon(A). amphibian(A) :- frog(A).
Учитывая приведенные выше факты и правила предикатов, мы можем делать запросы, например, находить всех известных теплокровных животных и выяснять, является ли предикат истинным или ложным для конкретного человека.
?- warm_blooded(A). A = b ; A = e ; A = o ; false ?- mammal(o). false ?- cold_blooded(s). true
Обратите внимание, что в первом запросе используется переменная, и Пролог дает успешную привязку переменной, которая делает запрос верным. Ввод точки с запятой заставляет Пролог вернуться назад, чтобы попытаться найти другое успешное связывание, что он и делает. Как только все возможные привязки найдены, возвращается false.
Рекурсия в Прологе
Рекурсивное мышление в Прологе ничем не отличается от мышления в других языках, даже несмотря на то, что Пролог — это совершенно другой язык, отличный от императивного или даже чисто функционального языка. Факты представляют базовые случаи. Правила предикатов, которые устанавливают условие для параметров предиката, чтобы получить определенное решение, также являются базовыми случаями, если они не вызывают рекурсивно предикат.
Для задачи суммирования первый базовый случай состоит в том, что сумма от 0 до 1 равна 1, представленной первым фактом. В другом базовом случае, который используется для предотвращения бесконечной рекурсии, а также для указания того, что сумма от 0 до 0 равна 0, используется правило со сравнением входной переменной N с 0. В последнем предложении правила используется разрез , !, что предотвращает возврат Пролога после того, как условие перед разрезом выполнено, и, следовательно, предотвращает использование рекурсивного случая, когда N меньше или равно 0.
sum(1,1). sum(N,0) :- N =< 0, !.
Рекурсивные правила используются для определения того, как использовать решение рекурсивного предиката для получения общего решения. В случае задачи суммирования правило должно выполнять некоторые арифметические действия: во-первых, установить переменную M на единицу меньше входной N, а во-вторых, вычислить сумму N и рекурсивного решения R, которое становится общим решением, S. (Сокращение в последнем предложении предотвращает дублирование решения, когда Prolog запрашивает альтернативное решение.)
sum(N,S) :- M is N-1, sum(M,R), S is N+R, !.
Запуск запроса дает ответ для определенного значения N или для того, является ли конкретная сумма правильным ответом для конкретного N:
?- sum(10,Answer). Answer = 55 ; false ?- sum(10,56). false
Обратите внимание, что из-за выполняемых арифметических операций запросы, в которых N — переменная, а сумма — конкретное значение, приведут к ошибке, поскольку N-1 нельзя вычислить, когда N — переменная, не связанная с конкретным значением.
Обычное первое упражнение на Прологе
Пролог обычно не используется для алгоритмов, включающих вычисление числовых значений и вывод их на экран. Хотя эти алгоритмы можно реализовать на Прологе, они кажутся неуклюжими. Отличительной чертой Пролога являются алгоритмы, основанные на логике, где программы составляются с использованием логических утверждений «истины», как показано в простом примере с классификацией животных.
Обычно первое реальное упражнение, которое дается студентам, изучающим Пролог, — это кодирование генеалогического дерева с использованием фактов и правил предикатов для определения отношений между людьми в дереве.

В следующем простом примере есть шесть известных людей, представленных символами от a до f, которые появляются в факте person. Есть также три факта, указывающие на то, что родителями элементов c и d являются a и b и родителями элемента f являются c и e. Обратите внимание, что база данных не включает информацию о родителях a, b и e.
%% The Facts %% person(a). person(b). person(c). person(d). person(e). person(f). parents(a,b,c). parents(a,b,d). parents(c,e,f).
Для удобства определен предикат для ответа на запросы вида: «является ли родитель Child равным Parent?» или «кто является родителем ребенка?» Предикат принимает входные данные в крайнем левом параметре (параметрах) и предоставляет решение в качестве крайнего правого параметра, но это просто соглашение. Есть два альтернативных способа удовлетворить этот предикат: либо родитель указан в качестве первого родителя для дочернего элемента в факте parents, либо родитель указан в качестве второго родителя для дочернего элемента в родители факт.
%% Convenience Predicate %% parentOf(Child,Parent) :- parents(Parent,_,Child) ; parents(_,Parent,Child).
Предикат, включающий рекурсию, используется для рассуждений о связи нескольких поколений между двумя людьми. Следуя тому же соглашению, которое упоминалось выше, этот предикат можно прочитать как «является ли предок потомка равным предку?» или «кто является предком Descendant?» Первое правило для предиката ancestorOf — это базовый случай, а именно, когда предикат parentOf истинен для Descendant и Ancestor, если одна или обе переменные были привязаны к значениям. (Если ни один из параметров не привязан к значению, Prolog выбирает факт parents из базы данных и связывает переменные в предикате parentOf, и, следовательно, ancestorOf к соответствующим значениям.)
%% Reasoning about Multi-Generational Relationships %%
ancestorOf(Descendant,Ancestor) :- parentOf(Descendant,Ancestor).
ancestorOf(Descendant,Ancestor) :- parentOf(Descendant,Middle),
ancestorOf(Middle,Ancestor).
Второе правило — это рекурсивный случай, который находит родителя Middle потомка, а затем рекурсивно проверяет ancestorOf, чтобы найти человека (или подтвердить), который является предком этого родителя. Поскольку первый пункт правила поднялся на одно поколение в генеалогическом древе, рекурсия приближается к базовому случаю нахождения прямых родительских отношений между двумя людьми и, таким образом, завершает рекурсию вверх по генеалогическому дереву. (Безуспешное исчерпание всех возможных фактов parents также завершит рекурсию.)
Поскольку арифметика здесь не задействована, предикат ancestorOf также можно использовать для ответов на вопросы вида «предком кого является Ancestor?» но эти вопросы звучали бы более естественно, как «кто является потомком Предка?» Другой удобный предикат, descendantOf, может быть определен, который просто меняет местами параметры при вызове предиката ancestorOf в правой части правила.
%% Another Convenience Predicate %% descendantOf(Ancestor,Descendant) :- ancestorOf(Descendant,Ancestor).
Имея небольшое генеалогическое древо в базе данных, мы можем выполнить несколько запросов, чтобы изучить отношения между людьми от a до f.

?- ancestorOf(f,Ancestor). Ancestor = c ; Ancestor = e ; Ancestor = a ; Ancestor = b ; false ?- ancestorOf(e,Ancestor). false
Первый запрос находит всех предков человека f, включая родителей c и e, а также бабушек и дедушек a. и б. Второй запрос показывает, что решение для предка e не найдено, поскольку в базе данных нет фактов parents, где e является потомком. .
?- descendantOf(d, Descendant). false ?- descendantOf(c, Descendant). Descendant = f ; false ?- descendantOf(b, Descendant). Descendant = c ; Descendant = d ; Descendant = f ; false
Точно так же в базе данных нет фактов parents, где d является родителем, поэтому первый запрос на поиск потомка d не может быть успешным. Второй запрос показывает, что использование предиката ancestorOf с известным предком, в данном случае c, работает просто отлично. Поиск всех потомков b также работает должным образом.
Для развлечения, давайте решим задачу о башне
Несмотря на то, что Пролог лучше подходит для программирования на основе логики, рекурсивные программы, включающие вывод, все же можно выполнять. Чтобы упростить чтение кода, выходной код можно поместить в собственное правило предиката и использовать, когда диск должен быть перемещен из исходной привязки в целевую привязку. Базовые случаи включают один для обеспечения того, чтобы количество дисков для башни было равно одному или более, и один для башни из одного диска, который просто сообщает о перемещении этого диска от источника к месту назначения.
%% Tower Helper and Base Cases %%
report_move(Source, Destination) :- write('move top disk on '),
write(Source), write(' to '),
write(Destination), nl.
move_tower(N,_,_,_) :- N < 1, write(‘ERROR’), nl, !.
move_tower(1,S,D,_) :- report_move(S,D).
Рекурсивный случай включает в себя два рекурсивных вызова башни с одним диском меньше, которые будут выводить ходы для перемещения меньшей башни, сначала из источника в запасной, а затем из запасного в пункт назначения. Между этими двумя вызовами происходит перемещение самого большого диска, нижней части общей башни, от источника к месту назначения.
%% Tower Recursive Case %%
move_tower(N,S,D,X) :- M is N-1,
move_tower(M,S,X,D),
report_move(S,D),
move_tower(M,X,D,S).
Запрос на 3-дисковую башню выводит инструкции по перемещению башни от начального к конечному стержню с использованием запасного стержня.
?- move_tower(3, starting, ending, spare). move top disk on starting to ending move top disk on starting to spare move top disk on ending to spare move top disk on starting to ending move top disk on spare to starting move top disk on spare to ending move top disk on starting to ending
Заключение
Для программистов, привыкших к императивным языкам и циклам, написание рекурсивного декларативного кода может сначала показаться странным. Ключом к программированию на Прологе является умение формулировать базовые и рекурсивные случаи для любых вычислений, которые в противном случае выполнялись бы с помощью цикла, а затем выражать их в виде фактов, нерекурсивных правил и рекурсивных правил, которые объявляются для предиката. Понимание того, как Пролог отвечает на запросы, используя унификацию переменных и поиск решений с возвратом, помогает демистифицировать то, как декларативный код выполняет нужные вычисления.
В следующий раз: рекурсия в списках!