Expressing Procedural Algorithms in Prolog Michael A. Covington Research Report 01-0012 Advanced Computational Methods Center University of Georgia Athens, Georgia 30602 May 26, 1986 4:25 pm -------------------------------------------------------------- This work was supported by PACER Grant 85PCR18 from Control Data Corporation and by Grant Number INS-85-02477 from the National Science Foundation. The opinions expressed here are solely those of the author. The assistance of Donald Nute and Andre Vellino is gratefully acknowledged. Printed copies of this and other research reports are available free on request. -------------------------------------------------------------- With the increasing popularity of Prolog as an application programming language, it is often necessary to develop Prolog equivalents for algorithms that were originally expressed in procedure-oriented languages. This paper presents a number of strategies for doing so. I assume that the reader is already familiar with the basic mechanisms of Prolog -- unification (matching), backtracking, and the like. Unless otherwise noted, the examples given here work both in full "Edinburgh" or "DEC-10" Prolog (Clocksin and Mellish 1984) and in Turbo Prolog (1986), which is a subset of the full language. The examples are written in Turbo Prolog syntax, but with declarations omitted for brevity. Full Prolog is used -- and noted as such -- when its features are needed. The procedural interpretation of logic Prolog is often described as a non-procedural language. In reality, it is a semi-procedural language -- a compromise between procedural and non-procedural programming, giving some of the advantages of each. In a truly non-procedural language, the programmer would provide only a logically rigorous set of conditions that the program must fulfill; the computer would then automatically generate an algorithm from them. Such things as the order in which the conditions were written would have no effect. In the interest of efficiency, Prolog contains some procedural elements. The Prolog programmer specifies not only the rules of inference to be used in solving a problem, but also the order in which they are to be tried. Crucially, the programmer can even specify that some potential paths to a solution should not be tried at all. This makes it possible to perform efficiently many 2 computations that would be severely inefficient, or even impossible, in pure Prolog. The key principle of Prolog is the procedural interpretation of logic. Consider the following Prolog rule set: [1] in_north_america(X) :- in_usa(X). in_usa(X) :- in_georgia(X). in_georgia(atlanta). This can be interpreted as a set of facts ([2] below) or as a set of procedure definitions ([3] below). [2] X is in North America if X is in the USA. X is in the USA if X is in Georgia. Atlanta is in Georgia. [3] To prove that X is in North America, prove that X is in the USA. To prove that X is in the USA, prove that X is in Georgia. To prove that Atlanta is in Georgia, do nothing. The unfamiliar task of drawing inferences from data is thereby reduced to the very familiar task of calling procedures. In what follows I will unashamedly refer to Prolog predicate definitions as procedures. Conditional execution One of the most important differences between Prolog and other programming languages is that, in general, Prolog procedures have multiple definitions (clauses), each applying under different conditions. In Prolog, conditional execution is expressed, not with if or case statements, but with these alternative definitions of procedures. Consider for example how we might translate into Prolog the following Pascal procedure: [4] procedure writename(X:integer); begin case X of 1: write('One'); 2: write('Two'); 3: write('Three') end end; This is done by giving writename three definitions: 3 [5] writename(1) :- write("One"). writename(2) :- write("Two"). writename(3) :- write("Three"). Each definition matches in exactly one of the three cases. A common mistake is to write the clauses as follows: [6] /* bad style */ writename(X) :- X=1, write("One"). writename(X) :- X=2, write("Two"). writename(X) :- X=3, write("Three"). This gives correct results but is needlessly inefficient. It is a waste of time to go into an inapplicable clause, perform a test that fails, backtrack out, and try another clause, when the use of the inapplicable clause could have been avoided in the first place. A key to effective programming in Prolog is making each logical unit of the program into a separate procedure. Each if or case statement should, in general, become a procedure call. For example, the hypothetical Pascal procedure: [7] procedure a(X:integer); begin b; if X=0 then c else d; e end; should go into Prolog as follows: [8] a(X) :- b, c_or_d(X), e. c_or_d(0) :- c. c_or_d(X) :- X<>0, d. This imposes a disciplined organization on the program that is even more rigorous than the structured ("go-to-less") programming that serves as the basis of Pascal, Ada, and C. Guarded command sets As Kluzniak and Szpakowicz (1985) have pointed out, Dijkstra's "guard" concept (Dijkstra 1975) is easily expressed in Prolog. A guarded command set is a structure of the form 4 [9] if C1 -> A1 C2 -> A2 C3 -> A3 fi where C1, C2, and C3 are conditions and A1, A2, and A3 are the corresponding actions. During program execution, all the conditions are evaluated. If all of the conditions are false, the program terminates. Otherwise, one of the actions corresponding to a true condition is selected for execution. If exactly one condition is true, the guarded command set is equivalent to an if-then or case statement. In fact, the Prolog procedure "c_or_d" in [8] can be expressed in more Dijkstra-like (though less efficient) form as: [10] c_or_d(X) :- /* condition */ X=0, /* action */ c. c_or_d(X) :- /* condition */ X<>0, /* action */ d. Guarded command sets were developed as part of a system for deriving algorithms from formal specifications of the problems they must solve. In Dijkstra's system, if more than one condition is true, no assumptions are made about which action will be taken. In Prolog, all possible actions will be taken on successive backtracking passes, in the order in which they are given in the program. The "cut" operator Let's consider another version of "writename" ([4] above) that includes a "catch-all" clause to deal with numbers whose names are not given. In many extended forms of Pascal, this can be expressed as: [11] procedure writename(X:integer); begin case X of 1: write('One'); 2: write('Two'); 3: write('Three') else write('Out of range') end end; One way to express this in Prolog is the following: 5 [12] writename(1) :- write("One"). writename(2) :- write("Two"). writename(3) :- write("Three"). writename(X) :- X<1, write("Out of range"). writename(X) :- X>3, write("Out of range"). This gives correct results but lacks conciseness. In order to make sure that only one clause is applicable to each number, we have had to add two more clauses. What we would like to say is that the program should print "Out of range" for any number that has not matched any of the first three clauses. We could try to express this as follows, with some lack of success: [13] /* incorrect */ writename(1) :- write("One"). writename(2) :- write("Two"). writename(3) :- write("Three"). writename(_) :- write("Out of range"). (Recall that the anonymous variable, written _, matches anything.) The problem here is that the goal "writename(1)", for example, matches both the first clause and the last clause. If a subsequent goal fails and causes backtracking through this one, the goal "writename(1)" will have two solutions, one that prints "One" and one that prints "Out of range." We want "writename" to be deterministic, that is, to give exactly one solution for any given set of parameters, and not give alternative solutions upon backtracking. We would therefore like to specify somehow that if any of the first three clauses succeeds, the computer should not try the last clause. This can be done with the "cut" operator (written as an exclamation mark). The cut operator commits the computer to take a particular solution (or potential solution) without trying alternatives. Suppose for example that we have the following rule set: [14] b :- c, d, !, e, f. b :- g, h. and that the current goal is "b". If we take the first clause and execute the cut, then it becomes impossible to look for alternative solutions to "c" and "d" (the goals that precede the cut in the same clause) or to "b" (the goal that invoked the clause containing the cut). It remains possible, of course, to backtrack all the way past "b" and look for alternatives to the clause that caused "b" to be invoked. What we need to do in [13] is to put a cut in each of the first three clauses. This changes their meaning slightly, so that the first clause (for example) says, "If the parameter is 1, then write 'One' and do not try any other clauses." 6 [15] writename(1) :- !, write("One"). writename(2) :- !, write("Two"). writename(3) :- !, write("Three"). writename(_) :- write("Out of range"). Since "write" is deterministic, it does not matter whether the cut is written before or after the call to "write". However, programs are usually more readable if cuts are made as early as possible. Making procedure calls always succeed or always fail In order to control the flow of program execution, it is often necessary to guarantee that a goal will always succeed regardless of the results of the computation that it performs. Occasionally, it is necessary to guarantee that a goal will always fail. An easy way to make any procedure succeed is to add an additional clause to it that succeeds with any parameters, and is tried last, thus: [16] f(X,Y) :- X < Y, !, write("X less than Y"). f(_,_). A call to "f" succeeds with any parameters; it may or may not print its message, but it will certainly not return failure and hence will not cause backtracking in the procedure that invoked it. Moreover, because of the cut, "f" is deterministic. The cut prevents the second clause from being used to generate a second solution with parameters that have already succeeded with the first clause. Similarly, a procedure can be guaranteed to fail by adding cut and fail at the end of each of its definitions, thus: [17] g(X,Y) :- X 0, M = N-1, factorial(M,FactM), FactN = N*FactM. This is straightforward; the procedure "factorial" calls itself to compute the factorial of the next smaller integer, then uses the result to compute the factorial of the integer in question. Now consider an iterative algorithm to do the same thing: [27] function factorial(N:integer):integer; var I,J:integer; begin I:=0; /* Initialize */ J:=1; while I