Дан фрагмент на Prolog: member(X,[X|_]). member(X,[_|T]) :- member(X,T). Объясните принцип работы поиска (backtracking) в этом примере, и приведите случай, когда порядок правил или аргументов приведёт к бесконечному поиску
Кратко о самом коде member(X,[X|_]). member(X,[_|T]) :- member(X,T). 1) Как идёт поиск (backtracking) - При вызове запроса Prolog пробует первую подходящую по унификации клаузу, затем при успехе возвращает ответ; при последующем backtracking идёт к следующей клаузе и т. д. - Пример: запрос `member(a,[b,a,c])`. Шаги: 1. Пробует первую клаузу: унификация X=aX=aX=a с головой \([X|_]=[b|_']\) неудачна ( a≠ba\neq ba=b ). 2. Переходит ко второй: [b,a,c]=[∣T][b,a,c]=[_|T][b,a,c]=[∣T] даёт T=[a,c]T=[a,c]T=[a,c], рекурсивно вызывает `member(a,[a,c])`. 3. Внутри рекурсии первая клаузa теперь удачна: [a,c]=[X∣][a,c]=[X|_][a,c]=[X∣] даёт X=aX=aX=a — успех, возвращается ответ. 4. При backtracking Prolog попытается найти другие варианты: внутри рекурсии других вариантов в первой клаузе уже нет, возвращается наверх и пробует, есть ли ещё варианты при предыдущих унификациях (например другие места в списке) — он продолжит искать дальше по второй клаузе, пока список не исчерпается. 2) Когда возникает бесконечный поиск a) Порядок правил - Если поставить рекурсивную клаузу раньше базовой: member(X,[_|T]) :- member(X,T). member(X,[X|_]). - Запрос `member(X,L)` (с LLL неинициализированным) будет приводить к бесконечной рекурсии: Prolog сначала всегда выбирает рекурсивную клаузу, унифицирует L=[G1∣T]L=[_G1|T]L=[G1∣T] и вызывает `member(X,T)` с TTT всё ещё неинициализированным, затем снова и снова — цепочка T=[G2∣T2]T=[_G2|T2]T=[G2∣T2], T2=[G3∣T3]T2=[_G3|T3]T2=[G3∣T3] … никогда не достигается базовый случай, потому что он стоит после рекурсии. Пример унификаций: L=[G1∣T], T=[G2∣T2], T2=[G3∣T3],…L=[_G1|T],\;T=[_G2|T2],\;T2=[_G3|T3],\dotsL=[G1∣T],T=[G2∣T2],T2=[G3∣T3],… — бесконечный спуск. b) Порядок аргументов (ошибка в рекурсивном вызове) - Если по опечатке поменять аргументы в рекурсивном вызове, например: member(X,[X|_]). member(X,[_|T]) :- member(T,X). то предикат начинает вызывать себя с несовместными позициями аргументов, что может привести к бесконечной рекурсии или к бессмысленному разрастанию термов в зависимости от запроса. Например запрос с незаполненными переменными легко породит нескончаемую генерацию некорректных унификаций. Вывод/рекомендация: базовый (не рекурсивный) случай нужно ставить раньше рекурсивного; в рекурсивных вызовах следить за тем, чтобы аргументы передавались в той же роли, в которой они ожидались, иначе возможно зацикливание.
member(X,[X|_]).
member(X,[_|T]) :- member(X,T).
1) Как идёт поиск (backtracking)
- При вызове запроса Prolog пробует первую подходящую по унификации клаузу, затем при успехе возвращает ответ; при последующем backtracking идёт к следующей клаузе и т. д.
- Пример: запрос `member(a,[b,a,c])`. Шаги:
1. Пробует первую клаузу: унификация X=aX=aX=a с головой \([X|_]=[b|_']\) неудачна ( a≠ba\neq ba=b ).
2. Переходит ко второй: [b,a,c]=[∣T][b,a,c]=[_|T][b,a,c]=[∣ T] даёт T=[a,c]T=[a,c]T=[a,c], рекурсивно вызывает `member(a,[a,c])`.
3. Внутри рекурсии первая клаузa теперь удачна: [a,c]=[X∣][a,c]=[X|_][a,c]=[X∣] даёт X=aX=aX=a — успех, возвращается ответ.
4. При backtracking Prolog попытается найти другие варианты: внутри рекурсии других вариантов в первой клаузе уже нет, возвращается наверх и пробует, есть ли ещё варианты при предыдущих унификациях (например другие места в списке) — он продолжит искать дальше по второй клаузе, пока список не исчерпается.
2) Когда возникает бесконечный поиск
a) Порядок правил
- Если поставить рекурсивную клаузу раньше базовой:
member(X,[_|T]) :- member(X,T).
member(X,[X|_]).
- Запрос `member(X,L)` (с LLL неинициализированным) будет приводить к бесконечной рекурсии: Prolog сначала всегда выбирает рекурсивную клаузу, унифицирует L=[G1∣T]L=[_G1|T]L=[G 1∣T] и вызывает `member(X,T)` с TTT всё ещё неинициализированным, затем снова и снова — цепочка T=[G2∣T2]T=[_G2|T2]T=[G 2∣T2], T2=[G3∣T3]T2=[_G3|T3]T2=[G 3∣T3] … никогда не достигается базовый случай, потому что он стоит после рекурсии. Пример унификаций: L=[G1∣T], T=[G2∣T2], T2=[G3∣T3],…L=[_G1|T],\;T=[_G2|T2],\;T2=[_G3|T3],\dotsL=[G 1∣T],T=[G 2∣T2],T2=[G 3∣T3],… — бесконечный спуск.
b) Порядок аргументов (ошибка в рекурсивном вызове)
- Если по опечатке поменять аргументы в рекурсивном вызове, например:
member(X,[X|_]).
member(X,[_|T]) :- member(T,X).
то предикат начинает вызывать себя с несовместными позициями аргументов, что может привести к бесконечной рекурсии или к бессмысленному разрастанию термов в зависимости от запроса. Например запрос с незаполненными переменными легко породит нескончаемую генерацию некорректных унификаций.
Вывод/рекомендация: базовый (не рекурсивный) случай нужно ставить раньше рекурсивного; в рекурсивных вызовах следить за тем, чтобы аргументы передавались в той же роли, в которой они ожидались, иначе возможно зацикливание.