Понимание списков прологов - PullRequest
0 голосов
/ 05 января 2019

Я пытаюсь понять списки Пролога и как 'возвращаются' / создаются значения в конце рекурсивной функции.

Я смотрю на этот простой пример:

val_and_remainder(X,[X|Xs],Xs).
val_and_remainder(X,[Y|Ys],[Y|R]) :-
   val_and_remainder(X,Ys,R).

Если я позвоню val_and_remainder(X, [1,2,3], R)., я получу следующие выводы:

X = 1, R = [2,3]; 
X = 2, R = [1,3];
X = 3, R = [1,2];
false.

Но я не совсем понимаю, почему в базовом случае (val_and_remainder(X,[X|Xs],Xs).) Xs должно отображаться так, как оно выглядит.

Если бы я должен был позвонить val_and_remainder(2, [1,2,3], R)., то мне кажется, что он будет проходить через программу как:

% Initial call
val_and_remainder(2, [1,2,3], R).

val_and_remainder(2, [1|[2,3]], [1|R]) :- val_and_remainder(2, [2,3], R).

% Hits base case
val_and_remainder(2, [2|[3]], [3]).

Если вышеуказанный прогон корректен, то как получить правильное значение для R? Как и в приведенном выше случае, значение R должно составлять R = [1,3].

Ответы [ 4 ]

0 голосов
/ 10 января 2019

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

foo( H, [H | T], T).                          % 1st clause

foo( X, [H | T], [H | R]) :- foo( X, T, R).   % 2nd clause

Так что это встроенный select/3. Ура! ..

Теперь вы спрашиваете о запросе foo( 2, [1,2,3], R) и как R правильно устанавливает его значение. Главное, чего не хватает в вашем кратком изложении, - это переименование переменных, когда выбрано соответствующее предложение. разрешение запроса выглядит так:

|-  foo( 2, [1,2,3], R) ? { } 
  %% SELECT -- 1st clause, with rename
  |-  ?  { foo( H1, [H1|T1], T1) = foo( 2, [1,2,3], R) }
         **FAIL** (2 = 1)
         **BACKTRACK to the last SELECT**
  %% SELECT -- 2nd clause, with rename
  |-  foo( X1, T1, R1) ?
         { foo( X1, [H1|T1], [H1|R1]) = foo( 2, [1,2,3], R) }
         **OK**
    %% REWRITE
    |-  foo( X1, T1, R1) ?
          { X1=2, [H1|T1]=[1,2,3], [H1|R1]=R }
    %% REWRITE
    |-  foo( 2, [2,3], R1) ?  { R=[1|R1] } 
      %% SELECT -- 1st clause, with rename
      |-  ? { foo( H2, [H2|T2], T2) = foo( 2, [2,3], R1), R=[1|R1] }
             ** OK ** 
        %% REWRITE
        |-  ? { H2=2, T2=[3], T2=R1, R=[1|R1] }
        %% REWRITE
        |-  ? { R=[1,3] }
        %% DONE

Цели между |- и ? являются резольвентой , уравнения внутри { } являются заменой . База знаний (КБ) неявно слева от |- в полном объеме.

На каждом шаге выбирается самая левая цель в резольвенте, а пункт с соответствующей головкой выбирается среди тех, что в КБ (в то время как переименовывает все Переменные предложения в согласованном порядке, так что никакая переменная в резольвенте не используется переименованным предложением, поэтому нет случайного захвата переменной) , и выбранная цель заменяется в резольвенте телом этого предложения, в то время как успешное объединение добавляется в замену. Когда резольвента пуста, запрос был проверен, и мы видим успешный и -ответвление в целом и / или дерево .


Вот как машина может это делать. Шаги «переписать» представлены здесь для облегчения понимания человеком.

Итак, мы видим здесь, что первый успешный выбор предложения приводит к уравнению

     R = [1 | R1     ]

, а второй, -

              R1 = [3]

, что в совокупности влечет за собой

     R = [1,        3]

Эта постепенная нисходящая инстанцияция / списывание списков - очень характерный способ Пролога действовать.


В ответ на вызов за вознаграждение, касающийся функциональной зависимости в отношении foo/3 (то есть select/3): в foo(A,B,C) любые два основных значения для B и C однозначно определяют значение A (или его отсутствие):

2 ?- foo( A, [0,1,2,1,3], [0,2,1,3]).
A = 1 ;
false.

3 ?- foo( A, [0,1,2,1,3], [0,1,2,3]).
A = 1 ;
false.

4 ?- foo( A, [0,1,2,1,3], [0,1,2,4]).
false.

f ?- foo( A, [0,1,1], [0,1]).
A = 1 ;
A = 1 ;
false.

Попытка опровергнуть его контраргументом:

10 ?- dif(A1,A2), foo(A1,B,C), foo(A2,B,C).
Action (h for help) ? abort
% Execution Aborted

Прологу не удается найти контраргумент.

Стремление более внимательно увидеть, что происходит, с итеративным углублением:

28 ?- length(BB,NN), foo(AA,BB,CC), XX=[AA,BB,CC], numbervars(XX), 
      writeln(XX), (NN>3, !, fail).
[A,[A],[]]
[A,[A,B],[B]]
[A,[B,A],[B]]
[A,[A,B,C],[B,C]]
[A,[B,A,C],[B,C]]
[A,[B,C,A],[B,C]]
[A,[A,B,C,D],[B,C,D]]
false.

29 ?- length(BB,NN), foo(AA,BB,CC), foo(AA2,BB,CC), 
      XX=[AA,AA2,BB,CC],  numbervars(XX), writeln(XX), (NN>3, !, fail).
[A,A,[A],[]]
[A,A,[A,B],[B]]
[A,A,[A,A],[A]]
[A,A,[A,A],[A]]
[A,A,[B,A],[B]]
[A,A,[A,B,C],[B,C]]
[A,A,[A,A,B],[A,B]]
[A,A,[A,A,A],[A,A]]
[A,A,[A,A,B],[A,B]]
[A,A,[B,A,C],[B,C]]
[A,A,[B,A,A],[B,A]]
[A,A,[A,A,A],[A,A]]
[A,A,[B,A,A],[B,A]]
[A,A,[B,C,A],[B,C]]
[A,A,[A,B,C,D],[B,C,D]]
false.

AA и AA2 всегда создаются для одной и той же переменной.

В числе 3 нет ничего особенного, поэтому с помощью обобщения можно с уверенностью предположить, что так будет всегда, при любой попытке.


Еще одна попытка прологологического доказательства:

ground_list(LEN,L):-
  findall(N, between(1,LEN,N), NS), 
  member(N,NS),
  length(L,N), 
  maplist( \A^member(A,NS), L).

bcs(N, BCS):- 
  bagof(B-C, A^(ground_list(N,B),ground_list(N,C),foo(A,B,C)), BCS).

as(N, AS):- 
  bagof(A, B^C^(ground_list(N,B),ground_list(N,C),foo(A,B,C)), AS).

proof(N):-
  as(N,AS), bcs(N,BCS), 
  length(AS,N1), length(BCS, N2), N1 =:= N2.

Сравнивает количество успешных комбинаций B-C в целом с количеством A s, которые они производят. Равенство означает однозначное соответствие.

И вот так,

2 ?- proof(2).
true.

3 ?- proof(3).
true.

4 ?- proof(4).
true.

5 ?- proof(5).
true.

И так для любого N. Все медленнее и медленнее. Общий неограниченный запрос для написания тривиален, но замедление кажется экспоненциальным.

0 голосов
/ 05 января 2019

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

Например, давайте возьмем такой простой случай:

same_term(X, X).

Это предикат , который определяет связь между двумя аргументами. Через объединение говорится, что первый и второй аргументы одинаковы, если они объединены (и это определение зависит от нас, авторов предиката). Таким образом, same_term(a, a) удастся, same_term(a, b) не удастся, а same_term(a, X) удастся с X = a.

Вы также можете написать это в более явной форме:

same_term(X, Y) :-
    X = Y.  % X and Y are the same if they are unified

Теперь давайте посмотрим на ваш пример, val_and_remainder/3. Во-первых, что это значит?

val_and_remainder(X, List, Rest)

Это означает, что X является элементом List, а Rest является списком, состоящим из всех остальных элементов (без X). (ПРИМЕЧАНИЕ: Вы не объяснили это значение сразу, но я определяю это значение из реализации вашего примера.)

Теперь мы можем написать, чтобы описать правила. Во-первых, простой базовый случай:

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

Это говорит о том, что:

Xs - остаток списка [X|Xs] без X.

Это утверждение должно быть довольно очевидно по определению синтаксиса [X|Xs] для списка в Прологе. Вам нужны все эти аргументы, потому что третий аргумент Xs должен объединяться с хвостом (остатком) списка [X|Xs], который также равен Xs (переменные с тем же именем, по определению, унифицированы). Как и раньше, вы можете написать это более подробно как:

val_and_remainder(X, [H|T], R) :-
    X = H,
    R = T.

Но краткая форма на самом деле более понятна.

Теперь рекурсивное предложение гласит:

val_and_remainder(X, [Y|Ys], [Y|R]) :- 
    val_and_remainder(X, Ys, R).

Итак, это значит:

[Y|R] - остаток списка [Y|Ys] без X , если R - остаток списка Ys без элемента X.

Вам нужно подумать об этом правиле, чтобы убедить себя, что оно логически верно. Y одинаков во втором и третьем аргументах, поскольку они ссылаются на один и тот же элемент, поэтому они должны объединяться.

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

Когда вы делаете запрос, например, val_and_remainder(2, [1,2,3], R)., Пролог проверяет, может ли объединить термин val_and_remainder(2, [1,2,3], R) с фактом или заголовком одного из ваших предикатных предложений. Он не может объединиться с val_and_remainder(X,[X|Xs],Xs), потому что ему нужно объединить X с 2, что означает, что ему нужно объединить [1,2,3] с [2|Xs], что не удалось, поскольку первый элемент [1,2,3] 1, но первый элемент [2|Xs] равен 2.

Таким образом, Prolog продолжает и успешно объединяет val_and_remainder(2, [1,2,3], R) с val_and_remainder(X,[Y|Ys],[Y|R]), объединяя X с 2, Y с 1, Ys с [2,3] и R с [Y|R] (ПРИМЕЧАНИЕ, это важно, переменная R в вашем вызове НЕ совпадает с переменной R в определении предиката, поэтому мы должны назвать это R1, чтобы избежать этой путаницы). Мы назовем ваш R как R1 и скажем, что R1 объединен с [Y|R].

Когда выполняется тело второго предложения, оно вызывает val_and_remainder(X,Ys,R). или, другими словами, val_and_remainder(2, [2,3], R). Теперь это объединится с первым предложением и даст вам R = [3]. Когда вы раскручиваете все это, вы получаете R1 = [Y|[3]] и вспоминаете, что Y был привязан к 1, в результате получается R1 = [1,3].

0 голосов
/ 05 января 2019

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

Вот различные подходы, которые вы всегда можете использовать:

Задайте самый общий запрос

... и пусть Пролог объяснит вам, о чем идет речь.

| ?- val_and_remainder(X, Xs, Ys).
     Xs = [X|Ys]
  ;  Xs = [_A,X|_B],
     Ys = [_A|_B]
  ;  Xs = [_A,_B,X|_C],
     Ys = [_A,_B|_C]
  ;  Xs = [_A,_B,_C,X|_D],
     Ys = [_A,_B,_C|_D]
  ;  Xs = [_A,_B,_C,_D,X|_E],
     Ys = [_A,_B,_C,_D|_E]
  ; ...

Итак, Xs и Ys имеют общий префикс списка, после этого Xs имеет X, за которым следует общий остаток. Этот запрос будет продолжать производить дальнейшие ответы. Иногда вы хотите увидеть все ответы, тогда вам нужно быть более конкретным. Но не будьте слишком конкретны:

| ?- Xs = [_,_,_,_], val_and_remainder(X, Xs, Ys).
     Xs = [X,_A,_B,_C],
     Ys = [_A,_B,_C]
  ;  Xs = [_A,X,_B,_C],
     Ys = [_A,_B,_C]
  ;  Xs = [_A,_B,X,_C],
     Ys = [_A,_B,_C]
  ;  Xs = [_A,_B,_C,X],
     Ys = [_A,_B,_C]
  ;  false.

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

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

Таким образом, вместо val_and_remainder(2, [1,2,3], R). (который, очевидно, заставил вашу голову кружиться) лучше рассмотреть val_and_remainder(2, [1,2,3], [1,3])., а затем val_and_remainder(2, [2,3],[3]). С этой стороны это должно быть очевидно.

Прочитайте правила Пролога справа налево

См. Правила Пролога как правила производства. Таким образом, всякий раз, когда все имеет место в правой части правила, вы можете заключить, что слева. Таким образом, :- является представлением в начале 1970-х гг.

Позже вы можете подумать и о более сложных вопросах. Как

Функциональные зависимости

Первый и второй аргументы однозначно определяют последний? X, Xs & rarr; Ys держать?

Вот пример запроса, который запрашивает, чтобы Ys и Ys2 были разными для тех же X и Xs.

| ?- val_and_remainder(X, Xs, Ys), val_and_remainder(X, Xs, Ys2), dif(Ys,Ys2).
     Xs = [X,_A,X|_B],
     Ys = [_A,X|_B],
     Ys2 = [X,_A|_B],
     dif([_A,X|_B],[X,_A|_B])
  ;  ...

Таким образом, очевидно, существуют различные значения для Ys для заданных X и Xs. Вот конкретный пример:

| ?- val_and_remainder(x, [x,a,x], Ys).
     Ys = [a,x]
  ;  Ys = [x,a]
  ;  false.

Здесь нет классического возвращения. Возвращается не один раз, а дважды. Это больше yield.

Тем не менее, является на самом деле функциональной зависимостью между аргументами! Вы можете найти это? И можете ли вы Прологом доказать это (так же, как Пролог может действительно доказать).

0 голосов
/ 05 января 2019

Из комментария:

Как верен результат R, потому что, если вы посмотрите на мой прогон вызова программы, значение Xs не [1,3], что в итоге выходы; вместо этого [3] объединяется с R (ясно, что я что-то упущено по пути, но я не уверен, что это такое).

Это правильно:

% Initial call
val_and_remainder(2, [1,2,3], R).

val_and_remainder(2, [1|[2,3]], [1|R]) :- val_and_remainder(2, [2,3], R).

% Hits base case
val_and_remainder(2, [2|[3]], [3]).

однако Prolog не похож на другие языки программирования, где вы входите с помощью input и выходите с помощью output в операторе return. В Прологе вы продвигаетесь вперед через предикаты, объединяющие и продолжающие с предикатами, которые являются истинными, а после возврата также объединяющие несвязанные переменные. (Это технически неверно, но некоторым легче понять, если вы думаете об этом так.)

Вы не учли несвязанные переменные, которые теперь связаны при возврате.

При попадании в базовый вариант Xs был привязан к [3],

но когда вы возвращаетесь назад, вы смотрите на

val_and_remainder(2, [1|[2,3]], [1|R])

и, в частности, [1|R] для третьего параметра.

Поскольку Xs было объединено с R в вызове к базовому случаю, т.е.

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

R теперь имеет [3].

Теперь третья позиция параметра в

val_and_remainder(2, [1|[2,3]], [1|R])

- это [1|R], то есть [1|[3]], что как синтаксический сахар [1,3], а не просто [3].

Теперь при запросе

val_and_remainder(2, [1,2,3], R).

был выполнен, третий параметр запроса R был объединен с третьим параметром предиката

val_and_remainder(X,[Y|Ys],[Y|R])

, поэтому R был объединен с [Y|R], который отменяет возврат на [1,3] и, следовательно, значение, связанное с переменной запроса R, равно [1,3]

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...