3

I have the following code, working, that shuffles two lists:

shuffle([], [], []).
shuffle([X|Xs], Ys, [X|Zs]):-
          shuffle(Xs, Ys, Zs).
shuffle(Xs, [Y|Ys], [Y|Zs]):-
          shuffle(Xs, Ys, Zs).

I understand each part separately. The first clause receives two lists, one with X that is the head, Xs is the tail. In the result we "take" only the first list's head. The same with the second clause – we don't take Xs for the result, only the head of Y.

Prolog recursively separates the lists and then unifies them.

What I don't understand here is how it works? After it ended "taking out" all the Xs, it just "moves" to the second clause, to take the Ys? What triggers Prolog to do that?

Thank you.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
Assaf
  • 1,112
  • 4
  • 14
  • 35

2 Answers2

2

When you try to prove a goal in Prolog for example: shuffle([a],[c],L). what Prolog does is to search in the database to find rules that much with the predicate shuffle.

In this case both second and third rule occur, so you have two options-choice points as called in Prolog:

1st-choice point: We examine second rule: shuffle([X|Xs],Ys,[X|Zs]):- shuffle(Xs,Ys,Zs). and applying that in our goal we get [X|Xs] = [a] (so X = a, Xs = []), Ys = [c], and L is of the form [a|Zs], and finally recursively shuffle([],[c],Zs) is called. This goal now only matches third rule and we get Zs = [c|Zs'] and again recursively shuffle([],[],Zs') is called where now only first rule matches and we get Zs' = []. So from the first case examined we get Zs = [a,c]. Now we have left another case:

2nd-choice point: We examine third rule: shuffle(Xs,[Y|Ys],[Y|Zs]):- shuffle(Xs,Ys,Zs). and applying that in our goal we get Xs = [a], [Y|Ys] = [c] (so Y = c, Ys = []), and L is of the form [c|Zs], and finally recursively shuffle([a],[],Zs) is called. This goal now only matches second rule and we get Zs = [a|Zs'] and again recursively shuffle([],[],Zs') is called where now only first rule matches and we get Zs' = []. So from the second case examined we get Zs = [c,a].

So finally we get the two solutions. As you can see Prolog does a depth-first analysis on the choice points because it finds the first choice point and examines it and the goes on to the third and so on. The obvious problem here is that can you imagine the number of choice points for two-element lists e.g shuffle([a,b],[c,d],L) ?? It would be four choice points and for the general case of Xs,Ys the choice point are way too much.

coder
  • 12,832
  • 5
  • 39
  • 53
  • Thank you for explaining. I do understand that the solution here goes for a lot of options (and that's what is asked from me). Just to make it clear to me - In any program, prolog first examines the first choice, FULLY, and then goes to the second choice? – Assaf Sep 07 '18 at 08:11
  • 1
    Yes exactly, Prolog generates the choice points and does depth-first examination of each choice points. You may have seen that this process is generating the choice point tree and the path to find solutions is doing dfs traversal e.g here is an example that analyze that: https://www.cpp.edu/~jrfisher/www/prolog_tutorial/3_1.html You can find plenty tutorial that all explain the same thing (as in the link) and show the tree that is generated. – coder Sep 07 '18 at 08:20
1

Avoiding all the X and Y and Z parts, what can we say about the working code:

  1. You start with a query like shuffle([1,2],[a,b],L). and Prolog tries to solve it by solving the three shuffle rules.
  2. One shuffle rule can be solved on its own, but only for empty lists, the other two rely on solving another case of the shuffle rule.
  3. Whatever solution is found must go shuffle -> shuffle -> [shuffle....] -> empty lists. It must. If it could not match any shuffles at all, it would answer "false" and your code wouldn't work. If it bounced between shuffles forever it would infinite loop and give no answers and your code wouldn't work. It does work, so it must chain from the start, through a combination of shuffles, to the empty lists.

Prolog will try to solve from the top of the rules:

From the top:

A) shuffle([1,2],[a,b],L).  -no->  shuffle([],[],[]).
B) shuffle([1,2],[a,b],L).  -??->  shuffle([X|Xs],Ys,[X|Zs]):- shuffle(Xs,Ys,Zs).
B) shuffle([1,2],[a,b],L).  -??->  shuffle([X=1|Xs=[2]],Ys=[a,b],[X=1|Zs=??]) :- shuffle(Xs=[2],Ys=[a,b],Zs).

% A) fails as [1,2] does not match with []
% B) partially binds but is missing Zs. Solving to try and find the Zs is now:

shuffle(Xs=[2],Ys=[a,b],Zs).



From the top:

A) shuffle([2],[a,b],Zs).  -no->  shuffle([],[],[]).
B) shuffle([2],[a,b],Zs).  -??->  shuffle([X|Xs],Ys,[X|Zs]):- shuffle(Xs,Ys,Zs).
B) shuffle([2],[a,b],Zs).  -??->  shuffle([X=2|Xs=[]],Ys=[a,b],[X=2|Zs=??]):- shuffle(Xs,Ys,Zs).

% A) fails as [2] does not match with []
% B) partially binds but is missing Zs. Solving to try and find the Zs is now:

shuffle(Xs=[],Ys=[a,b],Zs).



From the top:

A) shuffle([],[a,b],Zs).  -no->  shuffle([],[],[]).
B) shuffle([],[a,b],Zs).  -no->  shuffle([X|Xs],Ys,[X|Zs]):- shuffle(Xs,Ys,Zs).
C) shuffle([],[a,b],Zs).  -??->  shuffle(Xs,[Y|Ys],[Y|Zs]):- shuffle(Xs,Ys,Zs).
C) shuffle([],[a,b],Zs).  -??->  shuffle(Xs=[],[Y=a|Ys=[b]],[Y=a|Zs=??]):- shuffle(Xs,Ys,Zs).

% A) fails as [a,b] does not match with the second []
% B) fails as [] does not match with [X|Xs]
% C) partially binds but is missing Zs. Solving to try and find the Zs is now:

shuffle([],[b],Zs).



From the top:

A) shuffle([],[b],Zs).  -no->  shuffle([],[],[]).
B) shuffle([],[b],Zs).  -no->  shuffle([X|Xs],Ys,[X|Zs]):- shuffle(Xs,Ys,Zs).
C) shuffle([],[b],Zs).  -??->  shuffle(Xs,[Y|Ys],[Y|Zs]):- shuffle(Xs,Ys,Zs).
C) shuffle([],[b],Zs).  -??->  shuffle(Xs=[],[Y=b|Ys=[]],[Y=b|Zs=??]):- shuffle(Xs,Ys,Zs).

% A) fails as [b] does not match with the second []
% B) fails as [] does not match with [X|Xs]
% C) partially binds but is missing Zs. Solving to try and find the Zs is now:

shuffle([],[],Zs).



From the top:

A) shuffle([],[],Zs).  -no->  shuffle([],[],[]).

% A) succeeds. Zs can be []

This is a finished chain from the origin, through four shuffles, to the empty lists. During this chain, Zs has been constructed as [1|?] then [1|[2|?]] then [1|[2|[a|?]]] then [1|[2|[a|[b|?]]]] then [1|[2|[a|[b|[]]]]] which is complete, no missing anything. That binds to your L input for the first result.

It's gone through the shuffles B B C C.


But the search space is not exhausted, there might be more answers. If you ask for them it unwinds back up the chain to a place where it could have taken a different path. Instead of solving shuffle([X|Xs].. it can skip that one and dive down shuffle(Xs instead.

The two shuffle predicates with lots of values work together make a bounce pattern, which terminates with the three empty list cases:

[1,2],[a,b],Unknown
        \
         \
          \ ? shuffle shuffle shuffle
          /
         /
         \
      [],[],[]

One chain of logical connections is B B C C A. Another chain is B C B C A which results in the next answer L=[1,a,2,b].

[1,2],[a,b],Unknown
       /   \       
      /     \
      \      \ B C B A
B B C C\     /
       |    /
       |    \
      [],[],[]

Once it's backtracked all the way back, at each choice swapped the shuffle for the other one and followed down the chain to the empty lists, it will have found 6 paths, 6 ways to bounce through the shuffles.

As you make the lists longer, the chains get longer. When it starts backtracking up the chain undoing its steps looking for other ways to go, there are more of them. More choice points, so it will find more solutions - proportional to the length of the inputs.

TessellatingHeckler
  • 27,511
  • 4
  • 48
  • 87
  • Thank you for this explanation! fantastic. Can you please, if you have time, look on my other question - https://stackoverflow.com/questions/52219327/cant-understand-why-is-prolog-looping-infinitly ? – Assaf Sep 07 '18 at 09:21
  • 1
    I set out with "*I don't know how it works, but I'm hoping that by trying to explain it, I'll come to understand it*". A couple of hours and a couple of thousand words of writing and rewriting later, I think I do understand it. I suspected it always zig-zagged between the shuffles, it doesn't. I didn't know where the backtracking would kick in, now I do. I didn't know where the choicepoints came into it, now I do. Not sure my answer is any help to anyone else, but it helped me! :) – TessellatingHeckler Sep 07 '18 at 09:24