Explanation of Prolog recursive procedure - prolog

I'd like someone to explain this procedure if possible (from the book 'learn prolog now'). It takes two numerals and adds them together.
add(0,Y,Y).
add(s(X),Y,s(Z)) :- add(X,Y,Z).
In principle I understand, but I have a few issues. Lets say I issue the query
?- add(s(s(0)), s(0), R).
Which results in:
R = s(s(s(0))).
Step 1 is the match with rule 2. Now X becomes s(0) and Y is still s(0). However Z (according to the book) becomes s(_G648), or s() with an uninstantiated variable inside it. Why is this?
On the final step the 1st rule is matched which ends the recursion. Here the contents of Y somehow end up in the uninstantiated part of what was Z! Very confusing, I need a plain english explanation.

First premises:
We have s(X) defined as the successor of X so basically s(X) = X+1
The _G### notation is used in the trace for internal variables used for the recursion
Let's first look at another definition of addition with successors that I find more intuitive:
add(0,Y,Y).
add(s(A),B,C) :- add(A,s(B),C).
this does basically the same but the recursion is easier to see:
we ask
add(s(s(0)),s(0),R).
Now in the first step prolog says thats equivalent to
add(s(0),s(s(0)),R)
because we have add(s(A),B,C) :- add(A,s(B),C) and if we look at the question A = s(0) and B=s(0). But this still doesn't terminate so we have to reapply that equivalency with A=0 and B=s(s(0)) so it becomes
add(0,s(s(s(0))),R)
which, given add(0,Y,Y). this means that
R = s(s(s(0)))
Your definition of add basically does the same but with two recursions:
First it runs the first argument down to 0 so it comes down to add(0,Y,Y):
add(s(s(0)),s(0),R)
with X=s(0), Y = s(0) and s(Z) = R and Z = _G001
add(s(0),s(0),_G001)
with X = 0, Y=s(0) and s(s(Z)) = s(G_001) = R and Z = _G002
add(0,s(0),_G002)
So now it knows that _G002 is s(0) from the definition add(0,Y,Y) but has to trace its steps back so _G001 is s(_G002) and R is s(_G001) is s(s(_G002)) is s(s(s(0))).
So the point is in order to get to the definition add(0,Y,Y) prolog has to introduce internal variables for a first recursion from which R is then evaluated in a second one.

If you want to understand the meaning of a Prolog program, you might concentrate first on what the relation describes. Then you might want to understand its termination properties.
If you go into the very details of a concrete execution as your question suggests, you will soon be lost in the multiplicity of details. After all, Prolog has two different interlaced control flows (AND- and OR-control) and in addition to that it has unification which subsumes parameter passing, assignment, comparison, and equation solving.
Brief: While computers execute a concrete query effortlessly for zillions of inferences, you will get tired after a screenful of them. You can't beat computers in that. Fortunately, there are better ways to understand a program.
For the meaning, look at the rule first. It reads:
add(s(X),Y,s(Z)) :- add(X,Y,Z).
See the :- in between? It is meant to symbolize an arrow. It is a bit unusual that the arrow points from right-to-left. In informal writing you would write it rather left-to-right. Read this as follows:
Provided, add(X,Y,Z) is true, then also add(s(X),Y,s(Z)) is true.
So we assume that we have already some add(X,Y,Z) meaning "X+Y=Z". And given that, we can conclude that also "(X+1)+Y=(Z+1)" holds.
After that you might be interested to understand it's termination properties. Let me make this very brief: To understand it, it suffices to look at the rule: The 2nd argument is only handed further on. Therefore: The second argument does not influence termination. And both the 1st and 3rd argument look the same. Therefore: They both influence termination in the same manner!
In fact, add/3 terminates, if either the 1st or the 3rd argument will not unify with s(_).
Find more about it in other answers tagged failure-slice, like:
Prolog successor notation yields incomplete result and infinite loop
But now to answer your question for add(s(s(0)), s(0), R). I only look at the first argument: Yes! This will terminate. That's it.

Let's divide the problem in three parts: the issues concerning instantiation of variables and the accumulator pattern which I use in a variation of that example:
add(0,Y,Y).
add(s(X),Y,Z):-add(X,s(Y),Z).
and a comment about your example that uses composition of substitutions.
What Prolog applies in order to see which rule (ie Horn clause) matches (whose head unifies) is the Unification Algorithm which tells, in particular, that if I have a variable, let's say, X and a funtor, ie, f(Y) those two term unify (there is a small part about the occurs check to...check but nevermind atm) hence there is a substitution that can let you convert one into another.
When your second rule is called, indeed R gets unified to s(Z). Do not be scared by the internal representation that Prolog gives to new, uninstantiated variables, it is simply a variable name (since it starts with '_') that stands for a code (Prolog must have a way to express constantly newly generated variables and so _G648, _G649, _G650 and so on).
When you call a Prolog procedure, the parameters you pass that are uninstantiated (R in this case) are used to contain the result of the procedure as it completes its execution, and it will contain the result since at some point during the procedure call it will be instantied to something (always through unification).
If at some point you have that a var, ie K is istantiated to s(H) (or s(_G567) if you prefer), it is still partilally instantiated and to have your complete output you need to recursively instantiate H.
To see what it will be instantiated to, have a read at the accumulator pattern paragraph and the sequent one, tho ways to deal with the problem.
The accumulator pattern is taken from functional programming and, in short, is a way to have a variable, the accumulator (in my case Y itself), that has the burden to carry the partial computations between some procedure calls. The pattern relies on recursion and has roughly this form:
The base step of the recursion (my first rule ie) says always that since you have reached the end of the computation you can copy the partial result (now total) from your accumulator variable to your output variable (this is the step in which, through unification your output var gets instantiated!)
The recursive step tells how to create a partial result and how to store it in the accumulator variable (in my case i 'increment' Y). Note that in the recursive step the output variable is never changed.
Finally, concerning your exemple, it follows another pattern, the composition of substitutions which I think you can understand better having thought about accumulator and instantiation via unification.
Its base step is the same as the accumulator pattern but Y never changes in the recursive step while Z does
It uses to unify the variable in Z with Y by partially instantiating all the computation at the end of each recursive call after you've reached the base step and each procedure call is ending. So at the end of the first call the inner free var in Z has been substituted by unification many times by the value in Y.
Note the code below, after you have reached the bottom call, the procedure call stack starts to pop and your partial vars (S1, S2, S3 for semplicity) gets unified until R gets fully instantiated
Here is the stack trace:
add(s(s(s(0))),s(0),S1). ^ S1=s(S2)=s(s(s(s(0))))
add( s(s(0)) ,s(0),S2). | S2=s(S3)=s(s(s(0)))
add( s(0) ,s(0),S3). | S3=s(S4)=s(s(0))
add( 0 ,s(0),S4). | S4=s(0)
add( 0 ,s(0),s(0)). ______|

Related

What does this program in Prolog do?

What is the purpose of this:
route(Town1,Town2,Distance):-
road(Town1,Town2,Distance).
Also, what does ! mean in this context? If someone knows exactly what it means, because all I hear is "cut" without any other explanation.
%TRAVELLING SALESMAN PROBLEM
DOMAINS
town = symbol
distance = integer
PREDICATES
nondeterm road(town,town,distance)
nondeterm route(town,town,distance)
CLAUSES
road("tampa","houston",200).
road("gordon","tampa",300).
road("houston","gordon",100).
road("houston","kansas_city",120).
road("gordon","kansas_city",130).
route(Town1,Town2,Distance):-
road(Town1,Town2,Distance).
route(Town1,Town2,Distance):-
road(Town1,X,Dist1),
route(X,Town2,Dist2),
Distance=Dist1+Dist2, !.
GOAL
route("tampa", "kansas_city", X),
write("Distance from Tampa to Kansas City is ",X),nl.
Prolog programs use recursion heavily, which is why you need a base case for the recursion to stop. Here:
route(Town1,Town2,Distance):-
road(Town1,Town2,Distance).
We're saying "If there's a direct road between Town1 and Town2, then we're done, no need to check for a route through any other town because this is a route, just give me the distance." In other words, we are saying "If there is a defined clause that matches the first two arguments, what must Distance be to fully match it?"
The ! is the cut/1 predicate. It stops backtracking. Here it is, described more thoroughly:
Sometimes it is desirable to selectively turn off backtracking. Prolog provides a predicate that performs this function. It is called the cut/1, represented by an exclamation point (!).
The cut/1 effectively tells Prolog to freeze all the decisions made so far in this predicate. That is, if required to backtrack, it will automatically fail without trying other alternatives.
In the case of your code:
route(Town1,Town2,Distance):-
road(Town1,X,Dist1),
route(X,Town2,Dist2),
Distance=Dist1+Dist2, !.
You are telling Prolog that, if you find a road between Town1 and Town X, and then from Town X there is a route to finish of the path between Town1 and Town2, then recurse; if there is a direct road between X and Town2, then the predicate at the top of your question holds. If not, it will recursively loop. Eventually, Dist2 will have a final value which will float back up to this predicate.
This is where ! comes in. Once you have a route that holds, it says "Stop, don't try and find another route, I only want 1 route. Don't backtrack for any other possible routes." Otherwise, you'd get multiple write("Distance from Tampa to Kansas City is ",X),nl. with different X values, which might not have made sense to the author of this program.
Also, as #DavidTonhofer says, the = should be replaced with is as the latter will force arithmetic evaluation. For example X = 2+4 compares X to the unevaluated expression 2+4, but with is you'll be comparing X to 6.

Prolog and limitations of backtracking

This is probably the most trivial implementation of a function that returns the length of a list in Prolog
count([], 0).
count([_|B], T) :- count(B, U), T is U + 1.
one thing about Prolog that I still cannot wrap my head around is the flexibility of using variables as parameters.
So for example I can run count([a, b, c], 3). and get true. I can also run count([a, b], X). and get an answer X = 2.. Oddly (at least for me) is that I can also run count(X, 3). and get at least one result, which looks something like X = [_G4337877, _G4337880, _G4337883] ; before the interpreter disappears into an infinite loop. I can even run something truly "flexible" like count(X, A). and get X = [], A = 0 ; X = [_G4369400], A = 1., which is obviously incomplete but somehow really nice.
Therefore my multifaceted question. Can I somehow explain to Prolog not to look beyond first result when executing count(X, 3).? Can I somehow make Prolog generate any number of solutions for count(X, A).? Is there a limitation of what kind of solutions I can generate? What is it about this specific predicate, that prevents me from generating all solutions for all possible kinds of queries?
This is probably the most trivial implementation
Depends from viewpoint: consider
count(L,C) :- length(L,C).
Shorter and functional. And this one also works for your use case.
edit
library CLP(FD) allows for
:- use_module(library(clpfd)).
count([], 0).
count([_|B], T) :- U #>= 0, T #= U + 1, count(B, U).
?- count(X,3).
X = [_G2327, _G2498, _G2669] ;
false.
(further) answering to comments
It was clearly sarcasm
No, sorry for giving this impression. It was an attempt to give you a synthetic answer to your question. Every details of the implementation of length/2 - indeed much longer than your code - have been carefully weighted to give us a general and efficient building block.
There must be some general concept
I would call (full) Prolog such general concept. From the very start, Prolog requires us to solve computational tasks describing relations among predicate arguments. Once we have described our relations, we can query our 'knowledge database', and Prolog attempts to enumerate all answers, in a specific order.
High level concepts like unification and depth first search (backtracking) are keys in this model.
Now, I think you're looking for second order constructs like var/1, that allow us to reason about our predicates. Such constructs cannot be written in (pure) Prolog, and a growing school of thinking requires to avoid them, because are rather difficult to use. So I posted an alternative using CLP(FD), that effectively shields us in some situation. In this question specific context, it actually give us a simple and elegant solution.
I am not trying to re-implement length
Well, I'm aware of this, but since count/2 aliases length/2, why not study the reference model ? ( see source on SWI-Prolog site )
The answer you get for the query count(X,3) is actually not odd at all. You are asking which lists have a length of 3. And you get a list with 3 elements. The infinite loop appears because the variables B and U in the first goal of your recursive rule are unbound. You don't have anything before that goal that could fail. So it is always possible to follow the recursion. In the version of CapelliC you have 2 goals in the second rule before the recursion that fail if the second argument is smaller than 1. Maybe it becomes clearer if you consider this slightly altered version:
:- use_module(library(clpfd)).
count([], 0).
count([_|B], T) :-
T #> 0,
U #= T - 1,
count(B, U).
Your query
?- count(X,3).
will not match the first rule but the second one and continue recursively until the second argument is 0. At that point the first rule will match and yield the result:
X = [_A,_B,_C] ?
The head of the second rule will also match but its first goal will fail because T=0:
X = [_A,_B,_C] ? ;
no
In your above version however Prolog will try the recursive goal of the second rule because of the unbound variables B and U and hence loop infinitely.

How do recursive functions work in Prolog?

I was reading a recursive function in prolog that returns the Sum of all the elements in a list as below :
sum([ ], 0).
sum( [Elem | Tail], S):- sum(Tail, S1),
S is S1 + Elem.
I can't understand two issues:
1: In the left side of ":-" we have the Goal. It means all the calculations will be done in the right side of ":-" and then we can use the Goal as a normal function. It means we give our arguments and variables that the result will be putted on them, and the right side is responsible for calculating.
But in this code the Goal, itself is calculating the Head and Tail. I mean in my mind the code should have been like this (however it doesn't work!) :
sum(Tail, S1):-sum( [Elem | Tail], S),........
Because the goal is supposed to give the arguments and the right side is in charge of calculating.
2: I cannot understand how this code works step by step. can anyone give me a very simple example like how it calculates the sum of [1,2,3]?
In the left side of :- you have a head of the rule; on the right side you have the body of the rule. When the :- side and the body are missing, the rule becomes a fact.
It is not correct to say that the calculation is performed only in the body, because the decision making process of Prolog works with both sides of the rule. The concept where the head of the rule plays an important role is unification, a process by which the language decides which clauses are applicable to the query, and makes checks and temporary assignments of variables in the rule head to parts of the query ("unifies" them).
For example, when you query sum([1,2,3], X), Prolog checks both sum clauses, and decides that the query unifies only with the second one, because [] cannot be unified with [1,2,3].
Now it needs to unify [1,2,3] with [Elem | Tail] by making a temporary assignment that lasts for as long as we are in the body of the rule: Elem=1, and Tail = [2,3]. At this point it tries to solve sum again, passing [2,3] as the first parameter. The first rule does not match, so another temporary assignment of Elem=2 and Tail=[3] is made. In the third level of recursion we reach an assignment Elem=3 and Tail=[]. This is when we hit the first rule, producing an assignment of S1=0. Third level of invocation adds 3 to it; second level adds 2. First level adds 1, and returns with X set to 6.
An important part of prolog is matching (more generally, unification). When the Prolog run-time encounters a goal like sum(X,Y), it will try to match that term with the left-hand sides (head) of the rules for sum, in the order in which they appear. If the first rule fails, then the system will move to the second rule and so on.
In this case, the first head will only match if X is the empty list. If it is not empty, the match fails, and the next head is tried. This will succeed as long as X is any non empty list. Not only will it succeed, but it will bind the first element of the list to Elem , the rest of the list (which may be empty) to Tail. Since the second argument in the head of this rule is a variable, it will bind to whatever Y is.
Let's work through some examples:
sum([],X)?
First head matches, binding X to 0.
sum([1],X)?
First head does not match because [1] does not match []. Second one does with Elem <- 1, Tail<-[]. Therefore, we can proceed with the right-hand side of the rule:
sum(Tail,S1), S is S1 + Elem
Since Tail<-[] , the goal sum(Tail,S1) will yield the binding S1<-0 (see above). So with Elem<-1 and S1<-0, S1+Elem = 1.
And so on. Hopefully there is enough here for you to do the rest.
Your speculations about what happens during the execution are rather strange. No functions are involved at all.
For the evaluation of the goal sum([1,2,3], X), the second clause is selected because there is no matching between [1,2,3] (first argument of the goal) and [] (in the head of the first clause).
The matching instantiates Elem=1, Tail=[2,3] and S = X. Then the goal sum([2,3], S1) is evaluated, and succeeds (after recursion), returning the substitution S1=5. Then S=5+1 binds S to 6.

Prolog - Towers of Hanoi

I'm trying to write a program to solve the towers of hanoi problem in Prolog. None of the posts here helped me so I decided to ask for myself. I have written the following code. It works well for 2 disks but goes into an infinite loop for 3.
hanoi(1,A,B,_,[(A,B)]).
hanoi(X,A,B,C,Y):-
hanoi(X2,A,C,B,Y1),
hanoi(1,A,B,_,[Y2]),
hanoi(X2,C,B,A,Y3),
append(Y1,[Y2|Y3],Y),
X2 is X-1.
It is called in the following way:
?- hanoi(3, a, b, c, Y).
a,b,c are the pegs. 3 is the number of disks and X is where we want the result.
I need to get the result in Y. I'm trying to recursively find the moves for X-1 discs from peg 1 to 3 using 2, 1 disc from peg 1 to 2, X-1 discs from peg 3 to 2 and append them. I can't understand what I'm doing wrong. Any help or guidance would be appreciated! Thanks!
The obvious problem -
When you have a conjunction, like:
a, b, c
You have two readings of this, logical and procedural. The logical reading would be:
"The conjunction is true if a is true, and b is true, and c is true."
The procedural reading is:
"The conjunction will succeed if a is evaluated and succeeds, then b is evaluated and it also succeeds, and then c is evaluated and it also succeeds." (or, to put it in other words, do a depth-first search of the solution space)
If you are careful enough, the procedural reading is not necessary to argue about your code. However, this is only the case when all subgoals in the conjunction have full overlap of the logical and the procedural reading.
The offender here is is/2. It does not have a purely logical meaning. It evaluates the right-hand operand (the arithmetic expression) and unifies the result with the left-hand (usually an unbound variable).
Here, you have a conjunction (in the body of the second clause) that says, in effect, "evaluate a(X), then, if it succeeds, find the value of X". The obvious problem is that a(X) requires the value of X to be evaluated in such a way that it terminates.
So, move the is before all subgoals that use its result, or look into using constraints.

prolog - infinite rule

I have the next rules
% Signature: natural_number(N)/1
% Purpose: N is a natural number.
natural_number(0).
natural_number(s(X)) :-
natural_number(X).
ackermann(0, N, s(N)). % rule 1
ackermann(s(M),0,Result):-
ackermann(M,s(0),Result). % rule 2
ackermann(s(M),s(N),Result):-
ackermann(M,Result1,Result),
ackermann(s(M),N,Result1). % rule 3
The query is: ackermann (M,N,s(s(0))).
Now, as I understood, In the third calculation, we got an infinite search (failure branch). I check it, and I got a finite search (failure branch).
I'll explain: In the first, we got a substitution of M=0, N=s(0) (rule 1 - success!). In the second, we got a substitution of M=s(0),N=0 (rule 2 - success!). But what now? I try to match M=s(s(0)) N=0, But it got a finite search - failure branch. Why the compiler doesn't write me "fail".
Thank you.
It was a bit hard to understand exactly what Tom is asking here. Perhaps there's an expectation that the predicate natural_number/1 somehow influences the execution of ackermann/3. It will not. The latter predicate is purely recursive and makes no subgoals that depend on natural_number/1.
When the three clauses shown are defined for ackermann/3, the goal:
?- ackermann(M,N,s(s(0))).
causes SWI-Prolog to find (with backtracking) the two solutions that Tom reports, and then to go into infinite recursion (resulting in an "Out of Stack" error). We can be sure that this infinite recursion involves the third clause given for ackermann/3 (rule 3 per Tom's comments in code) because in its absence we only get the two acknowledged solutions and then explicit failure:
M = 0,
N = s(0) ;
M = s(0),
N = 0 ;
false.
It seems to me Tom asks for an explanation of why changing the submitted query to one that sets M = s(s(0)) and N = 0, producing a finite search (that finds one solution and then fails on backtracking), is consistent with the infinite recursion produced by the previous query. My suspicion here is that there's a misunderstanding of what the Prolog engine attempts in backtracking (for the original query), so I'm going to drill down on that. Hopefully it clears up the matter for Tom, but let's see if it does. Admittedly my treatment is wordy, but the Prolog execution mechanism (unification and resolution of subgoals) is worthy of study.
[Added: The predicate has an obvious connection to the famous Ackermann function that is total computable but not primitive recursive. This function is known for growing rapidly, so we need to be careful in claiming infinite recursion because a very large but finite recursion is also possible. However the third clause puts its two recursive calls in an opposite order to what I would have done, and this reversal seems to play a critical role in the infinite recursion we find in stepping through the code below.]
When the top-level goal ackermann(M,N,s(s(0))) is submitted, SWI-Prolog tries the clauses (facts or rules) defined for ackermann/3 until it finds one whose "head" unifies with the given query. The Prolog engine does not have far to look as the first clause, this fact:
ackermann(0, N, s(N)).
will unify, binding M = 0 and N = s(0) as has already been described as the first success.
If requested to backtrack, e.g. by user typing semi-colon, the Prolog engine checks to see if there is an alternative way to satisfy this first clause. There is not. Then the Prolog engine proceeds to attempt the following clauses for ackermann/3 in their given order.
Again the search does not have to go far because the second clause's head also unifies with the query. In this case we have a rule:
ackermann(s(M),0,Result) :- ackermann(M,s(0),Result).
Unifying the query and the head of this rule yields the bindings M = s(0), N = 0 in terms of the variables used in the query. In terms of the variables used in the rule as stated above, M = 0 and Result = s(s(0)). Note that unification matches terms by their appearance as calling arguments and does not consider variable names reused across the query/rule boundary as signifying identity.
Because this clause is a rule (having body as well as head), unification is just the first step in trying to succeed with it. The Prolog engine now attempts the one subgoal that appears in the body of this rule:
ackermann(0,s(0),s(s(0))).
Note that this subgoal comes from replacing the "local" variables used in the rule by the values of unification, M = 0 and Result = s(s(0)). The Prolog engine is now calling the predicate ackermann/3 recursively, to see if this subgoal can be satisfied.
It can, as the first clause (fact) for ackermann/3 unifies in the obvious way (indeed in essentially the same way as before as regards the variables used in the clause). And thus (upon this recursive call succeeding), we get the second solution succeeding in the outer call (the top-level query).
If the user asks the Prolog engine to backtrack once more, it again checks to see if the current clause (the second one for ackermann/3) can be satisfied in an alternative way. It cannot, and so the search continues by passing to the third (and last) clause for predicate ackermann/3:
ackermann(s(M),s(N),Result) :-
ackermann(M,Result1,Result),
ackermann(s(M),N,Result1).
I'm about to explain that this attempt does produce infinite recursion. When we unify the top-level query with the head of this clause, we get bindings for the arguments that can perhaps be clearly understood by aligning the terms in in the query with those in the head:
query head
M s(M)
N s(N)
s(s(0)) Result
Bearing in mind that variables having the same name in the query as variables in the rule does not constrain unification, this triple of terms can be unified. Query M will be head s(M), that is a compound term involving functor s applied to some as-yet unknown variable M appearing in the head. Same thing for query N. The only "ground" term so far is variable Result appearing in the head (and body) of the rule, which has been bound to s(s(0)) from the query.
Now the third clause is a rule, so the Prolog engine must continue by attempting to satisfy the subgoals appearing in the body of that rule. If you substitute values from the head unification into the body, the first subgoal to satisfy is:
ackermann(M,Result1,s(s(0))).
Let me point out that I've used here the "local" variables of the clause, except that I've replaced Result by the value to which it was bound in unification. Now notice that apart from replacing N of the original top-level query by variable name Result1, we are just asking the same thing as the original query in this subgoal. Certainly it's a big clue we might be about to enter an infinite recursion.
However a bit more discussion is needed to see why we don't get any further solutions reported! This is because the first success of that first subgoal is (just as found earlier) going to require M = 0 and Result1 = s(0), and the Prolog engine must then proceed to attempt the second subgoal of the clause:
ackermann(s(0),N,s(0)).
Unfortunately this new subgoal does not unify with the first clause (fact) for ackermann/3. It does unify with the head of the second clause, as follows:
subgoal head
s(0) s(M)
N 0
s(0) Result
but this leads to a sub-subgoal (from the body of the second clause):
ackermann(0,s(0),s(0)).
This does not unify with the head of either the first or second clause. It also does not unify with the head of the third clause (which requires the first argument to have the form s(_)). So we reached a point of failure in the search tree. The Prolog engine now backtracks to see if the first subgoal of the third clause's body can be satisfied in an alternative way. As we know, it can be (since this subgoal is basically the same as the original query).
Now M = s(0) and Result1 = 0 of that second solution leads to this for the second subgoal of the third clause's body:
ackermann(s(s(0)),N,0).
While this does not unify with the first clause (fact) of the predicate, it does unify with the head of the second clause:
subgoal head
s(s(0)) s(M)
N 0
0 Result
But in order to succeed the Prolog engine must satisfy the body of the second clause as well, which is now:
ackermann(s(s(0)),s(0),0).
We can easily see this cannot unify with the head of either the first or second clause for ackermann/3. It can be unified with the head of the third clause:
sub-subgoal head(3rd clause)
s(s(0)) s(M)
s(0) s(N)
0 Result
As should be familiar now, the Prolog engine checks to see if the first subgoal of the third clause's body can be satisfied, which amounts to this sub-sub-subgoal:
ackermann(s(0),Result1,0).
This fails to unify with the first clause (fact), but does unify with the head of the second clause binding M = 0, Result1 = 0 and Result = 0 , producing (by familiar logic) the sub-sub-sub-subgoal:
ackermann(0,0,0).
Since this cannot be unified with any of the three clauses' heads, this fails. At this point the Prolog engine backtracks to trying to satisfy the above sub-sub-subgoal using the third clause. Unification goes like this:
sub-sub-subgoal head(3rd clause)
s(0) s(M)
Result1 s(N)
0 Result
and the Prolog engine's task is then to satisfy this sub-sub-sub-subgoal derived from the first part of the third clause's body:
ackermann(0,Result1,0).
But this will not unify with the head of any of the three clauses. The search for a solution to the sub-sub-subgoal above terminates in failure. The Prolog engine backtracks all the way to where it first tried to satisfy the second subgoal of the third clause as invoked by the original top-level query, as this has now failed. In other words it tried to satisfy it with the first two solutions of the first subgoal of the third clause, which you will recall was in essence the same except for change of variable names as the original query:
ackermann(M,Result1,s(s(0))).
What we've seen above are that solutions for this subgoal, duplicating the original query, from the first and second clauses of ackermann/3, do not permit the second subgoal of the third clause's body to succeed. Therefore the Prolog engine tries to find solutions that satisfy the third clause. But clearly this is now going into infinite recursion, as that third clause will unify in its head, but the body of the third clause will repeat exactly the same search we just chased through. So the Prolog engine now winds up going into the body of the third clause endlessly.
Let me rephrase your question: The query ackermann(M,N,s(s(0))). finds two solutions and then loops. Ideally, it would terminate after these two solutions, as there is no other N and M whose value is s(s(0)).
So why does the query not terminate universally? Understanding this can be quite complex, and the best advice is to not attempt to understand the precise execution mechanism. There is a very simple reason: Prolog's execution mechanism turns out to be that complex that you easily will misunderstand it anyway if you attempt to understand it by stepping through the code.
Instead, you can try the following: Insert goals false at any place in your program. If the resulting program does not terminate, then also the original program will not terminate.
In your case:
ackermann(0, N, s(N)) :- false.
ackermann(s(M),0,Result):- false,
ackermann(M,s(0),Result).
ackermann(s(M),s(N),Result):-
ackermann(M,Result1,Result), false,
ackermann(s(M),N,Result1).
We can now remove the first and second clause. And in the third clause, we can remove the goal after false. So if the following fragment does not terminate, also the original program will not terminate.
ackermann(s(M),s(N),Result):-ackermann(M,Result1,Result), false.
This program now terminates only if the first argument is known. But in our case it's free...
That is: By considering a small fraction of the program (called a failure-slice) we already were able to deduce a property of the entire program. For details, see this paper and others on the site.
Unfortunately, that kind of reasoning only works for cases of non-termination. For termination, things are more complex. The best is to try a tool like cTI which infers termination conditions and tries to prove their optimality. I entered your program already, so try to modify if and see the effects!
If we are at it: This small fragment also tells us that the second argument does not influence termination1. That means, that queries like ackermann(s(s(0)),s(s(0)),R). will
not terminate either. Exchange the goals to see the difference...
1 To be precise, a term that does not unify with s(_) will influence termination. Think of 0. But any s(0), s(s(0)), ... will not influence termination.

Resources