UNDERSTANDING PROLOG (A LITTLE BETTER) by Jean-Luc Romano ------------------------------------------------------------------- INTRODUCTION You've tackled Java, taken on Lisp, and now you're learning Prolog. You've certainly proven that you can learn new programming languages, but Prolog doesn't strike you as being like any other programming language that you've seen before. If you find yourself struggling to understand even a very basic set of rules in Prolog, then perhaps this file will help you understand some fundamentals of Prolog. ------------------------------------------------------------------- UNDERSTANDING THE PROLOG PARADIGM: LOOKING AT THE WORLD THE WAY PROLOG SEES IT One of the areas that causes the most confusion in Prolog is that many users new to Prolog want to treat the Prolog functors like functions in C/C++, Java, or Lisp. Since Prolog functors look a lot like the functions in these other languages, it's easy to understand how this mistake might be made. Therefore, let's first look at this first difference. Let's define a function designed to find the square of a number. It might look like this: float square(float a) { return a*a; }; /* for C/C++ and Java */ (defun square (a) (* a a)) ; for Lisp Let's examine what's going on: basically, in each of the languages, we are passing in the value we want squared. The value is squared, then returned. Simple, isn't it? This is where Prolog is different. In Prolog, there are no return values. Values are simply passed into a functor as a statement, and then Prolog declares whether the statement can be proven true. Therefore, BOTH the value to be squared AND the result should be passed in; THEN we get to know if those values form a valid combination. To understand, review the following lines of code, noticing that they now return boolean values (instead of the square of the number): boolean square(float a, float b) { return (b == a*a;) } /* C/C++/Java */ (defun square (a b) (= b (* a a))) ; Lisp square(A,B) :- B is A * A. % Prolog You probably understand what the first two lines of code are doing, so let's review the Prolog line (the third line). When you query Prolog with a statement like square(3,9) you are asking Prolog if it can prove the statement as being true. Looking back at the code we typed in, we see that Prolog will state any statement that can bind to square(A,B) as being true if it can prove that the statement "B is A * A" is true. Therefore, when you type "square(3,9)." Prolog tries to prove that "9 is 3 * 3." Since this is a rather trivial mathematical operation for Prolog to prove, it will have no problem proving that statement as true. Because it determines that 9 is in fact equal to 3 * 3, it then displays "yes", meaning that it was able to prove square(3,9) as being true. A few things to note: - If Prolog ever displays "no" it does not necessarily mean that the statement is false, but rather that it was unable to be proven true. You will find, however, that sometimes Prolog will end up in an infinite loop (rather than displaying "yes" or "no") when it is unable to prove a true statement as true. - If you've ever wondered why the keyword "is" is used when comparing one mathematical value to another, it is because the equal sign ("=") is already reverved for the bind operation (for example, List1 = [a,b,c] means that the variable named "List1" binds to [a,b,c]). - Remember, Prolog never returns a value, it simply states whether or not it can prove the statement (with its parameters) as being true. Whereas in other languages you would write a function to return a desired value, in Prolog you would write code that, given all the parameters, would say that all the parameters and values fit together to make a true statement. This is one of Prolog's main differences from many other programming languages. ------------------------------------------------------------------- AN EXTRA FEATURE OF PROLOG There's a handy feature of Prolog that hasn't been mentioned yet in this file which you probably know about if you've studied even a little bit of Prolog. Not only will Prolog try to prove a statement (with all its parameters) as being true, but if you replace a parameter value with a variable, it will try to find a value for the variable that makes the statement true! To clarify, we all know that Prolog will say the following is true: square(6,36). We typed this already knowing that the square of 6 is 36. But what if we don't know that? What if we enter a huge number whose square we don't know? If this is the case, we can put a variable name (remember: variable names always start with a capital letter in Prolog!) in place of where the squared value should be, like this: square(17,SquaredValue). Prolog then says: SquaredValue = 289 This means that Prolog was able to able to prove "square(17,SquaredValue)" as true, provided that SquaredValue binds to 289. So, can you do it the other way around? In other words, can you put a variable name in for the first parameter, and have it show you the square root, like this?: square(SquareRoot,9). Well... sometimes, depending on how well you wrote your program. This particular example gives me an error message, most likely because Prolog wasn't made to "undo" a multiplication operation. Theoretically it COULD go through the set of all real numbers, but since it would have an infinite number of values to test, it's possible that Prolog would never show you any value at all. However, most code you will write in Prolog will deal with lists instead of mathematical operations (after all, if we wanted to use many mathematical operations, we would probably write our program in C/C++ or Lisp, wouldn't we?...). And when dealing with lists it's much easier to write code that works "the other way around," or that fills in the missing piece (the parameter that is replaced with a variable). But even here you need to be careful that you don't send Prolog into a near-infinite loop. ------------------------------------------------------------------- TRY THIS AT HOME Say you want to find the square of some number, for example, 15. Now type the following into Prolog: square(15). You will probably see something similar to the following message: ++Error: Undefined predicate: square / 1 Aborting... So what does this mean? Did we misspell the word "square"? Did we type the code for "square" incorrectly? Did we find a number that the functor does not work correctly with? Or did we just find a bug in Prolog? Actually, none of the above are correct. Do you know what's wrong? We made the error of confusing Prolog usage with C/C++, Java, and Lisp usage. If we were using those languages we would call "square" with one parameter, the parameter we want to be squared. However, we are using Prolog, and Prolog wants us to fill in BOTH parameters. We can fill in a parameter with a variable (which is what we want in this case), but we MUST supply both parameters. This is what we should have typed: square(15,What). Because you are so used to thinking in terms of C/C++, Java, and Lisp, you will probably encounter the above error quite often. So the next time you get that error message, don't tear your hair out trying to find the bug in your code (like I did), but remember that it probably happened because you were thinking about returning a value, when you should have entered in a variable for a parameter. This knowledge could save you hours (or at least minutes) of frustration in the future. WHAT KIND OF $%&@#@& FUNCTION DECLARATION IS THIS? Sometimes looking at Prolog code can be confusing to someone new to Prolog. Take the following line of code for the append functor: append([],L,L). If you were like me, the first time you saw this you thought something like, "What kind of variable declarations are those? And why doesn't this function have a body?" Clearly, we are used to thinking in terms of C/C++, Java, and Lisp, where every variable in the parameter list is given a unique name. That makes sense to us. But now we're using Prolog, and it's important to point out the differences. First of all, we're not defining a function, we're just telling Prolog that a query to append is true if it can bind to the above statement. Let's not try to understand WHY the line was written like that (we will cover that later). For now, let's just look at the code and try to understand WHAT it does. Let's rewrite the above append line to be the following: append(Param1,Param2,Param3) :- Param1 = [], Param2 = Param3. The two append lines are equivalent in that they both are saying the same thing, namely: "An append statement is true IF the first parameter binds to an empty list, and the second parameter binds to the third parameter." That's all it's saying. Therefore, Prolog will say that any append statement that fits that description is true. Here are some examples: append([],[1,2,3],[1,2,3]). append([],5,5). append([],elephant,elephant). You might think at first that the last example shouldn't work, but since it fits the description, Prolog says that it is true. Try it if you have trouble believing it! You can also try these examples with variables: append(What,elephant,elephant). % What binds to an empty list append([],What,elephant). % What binds to elephant append([],elephant,What). % What binds to elephant append(What,elephant,giraffe). % Doesn't bind. Can you see why? Now let's write code to find the head of a list. We must remember that we don't RETURN the head of a list, but rather pass it in as a parameter, along with the list itself: headOfList(First,List) :- List = [First|Rest]. This line of code is saying that a headOfList statement will be found to be true IF the variable named List can bind to a list that has the variable named First as its first element. So the following examples should make Prolog print "yes": headOfList(a,[a,b,c]). headOfList(1,[1,1,2,3]). headOfList(elephant,[elephant]). Now look back to the headOfList code. Do you see how we bind List to [First|Rest] after the ":-" operator? We can shorten the code by replacing the List variable on the left side of the ":-" with what List binds to (on the right side of the ":-" operator): headOfList(First,[First|Rest]). Essentially, this line of code is saying the exact same thing: "Evaluate the line as true IF the second parameter binds to a list that has the variable named First as its first element." Now you may be asking, "If both ways are exactly the same, why can't I use the method in which I manually make the variables bind on the right side of the ':-' operator? After all, the longer way is a lot easier for me to understand." Well, the answer is that the shorter method not only saves a few keystrokes (a big incentive, I know), but it also saves Prolog a few steps in reducing computation time. A few extra steps might not seem like a big deal, but keep in mind that Prolog may call the headOfList functor thousands of times. And if each time it is called there are a few extra steps, the computation time can drastically increase. So I recommend getting used to writing code the shorter way so you'll have an easier time identifying what it does the next time you run across it. ------------------------------------------------------------------- A HELPFUL TIP In the next section we will cover recursion, which is not a very easy topic, even if you're not new to recursion. From there, the complexity of the programs will increase, and there will be times that you forget exactly what a certain expression does. For this reason I offer the following tip: Before every functor you define, put a comment line in that shows an example usage -- an example that Prolog would determine to be true. For example, the code for headOfFirst defined above would look like: % headOfFirst example usage: headOfFirst(a,[a,b,c]). headOfList(First,[First|Rest]). This may seem trivial, but if you ever write code to find the head of a list for use in another functor you might accidentally revert back to the C/C++/Java/Lisp paradigm where you were accustomed to receiving a return value. Believe me on this one: having that one comment line is extremely helpful in understanding just what you're working towards. Make it a personal rule to always include it. ------------------------------------------------------------------- UNDERSTANDING RECURSION Now for the hard part... recursion! There's no avoiding it -- it's time to study it. Actually, programming with recursion isn't all that hard, it's trying to understand already written code that can prove to be difficult. In my opinion, the code for the member functor was so good that I'm going to use it here as a teaching tool. You may remember it from the short tutorial on Prolog. Let's begin by saying that we want to write code that tells us if a particular item is in a list. First of all, we should be clear on how we are going to define the functor. We want to make both the item and the list as parameters, and Prolog will tell us "yes" or "no" depending on whether or not the item is a member. Using "the helpful tip" mentioned above, we'll write some comment lines that shows example usages: % member example usage: member(a,[a,b,c,d,e]). % another member example usage: member(c,[a,b,c,d,e]). Now if we were programming in C/C++, Java, or Lisp we would probably declare a boolean variable, set it to the proper value, and then return it. But Prolog is different. Here we try to prove that correct values are true. So how do we go about proving this? Well, let's look at the first example usage. We know it is true because 'a' happens to be the first item of the list. So we can write the following line of code: member(Item,List) :- List = [Item|Rest]. This basically says that member(a,[a,b,c,d,e]) evaluates to true if the second parameter can bind to a list that has the first parameter as its first element. Replacing the second parameter with what it binds to, we shorten the code to: member(Item,[Item|Rest]). Okay, so now we proved the first example, but how do we prove the second example? Well, we were able to prove it true if the item just happened to be the first element in the list, but most of the time it won't be. So what we do is we repeatedly "peel off" the first element of the list until the first element is finally the item we're looking for. This "peeled off" item is given the variable name of "Disregard" in the following code: member(Item,LongList) :- member(Item,ShortList), LongList = [Disregard|ShortList]. This line just above will get called ONLY IF the item is not the first element of the list (if it were, the first line would tell Prolog that the expression is true). So when the item isn't the first on the list, the second line of code is used. This calls the member functor again with the same item but with a shorter list, in the hopes that now the item will be the first item in the list. If it now IS the first item in the lisp, Prolog will print "yes"; if not, Prolog will recursively call member with an even shorter list, and will continue doing so until it either finds the item at the head of the list, or ShortList can no longer bind to a shorter list, making Prolog print "no". WARNING! Although the above line of code LOOKS correct, there is something wrong with it. If you try out that code with a query like "member(c,[a,b,c,d,e])." you will end up in an infinite loop. Can you see why? This is because of "left recursion." Left-recursion means that the recursive call occurs to the LEFT of the other statements. This fact doesn't seem important, but what it's doing is telling Prolog to recursively call member before its parameters are bound to anything. In other words, member is called before there is a relationship established between the shorter list and the longer list. And when member is called again, it AGAIN calls member with a ShortList that isn't necessarily a shorter list at all. And member will keep calling itself forever because nothing ever tells it that it can't! In other words, ShortList can bind to whatever it wants to -- it won't be until after it returns from the member call that it may reject the bind... but we know that it will never return! So remember whenever you use recursion that "left recursion" is always a bad idea. Make sure the functor never gets called unless its parameters have already been bound. Knowing this, we will remove the left recursion by shortening the code. member(Item,LongList) :- member(Item,ShortList), LongList = [Disregard|ShortList]. now becomes: member(Item,[Disregard|ShortList]) :- member(Item,ShortList). Now put all the lines of code together: % member example usage: member(a,[a,b,c,d,e]). % another member example usage: member(c,[a,b,c,d,e]). member(Item,[Item|Rest]). member(Item,[Disregard|ShortList]) :- member(Item,ShortList). ...and, voila'! We now have code that determines if an item is in a list. We can run the examples to test our code if we want. ------------------------------------------------------------------- ANOTHER RECURSION EXAMPLE The code for append is so useful that I decided to include it in this file. But before we begin, let's think about what parameters we would use. Certainly, in C/C++, Java, and Lisp we would pass in two lists and return the combined list. But as we all know now, we are programming in Prolog, which requires that the combined list also be included as a parameter. So our append functor should take three parameters: the two lists we want to append, and the combined list. The next thing we should do is create a comment line that shows an example usage of append (that Prolog will say is a true statement): % append example usage: append([a,b,c],[1,2,3],[a,b,c,1,2,3]). Now we think to ourselves, "How do I go about proving that the example is correct?" One way to go about doing this is by noticing that both the first and the third parameters begin with the same elements. By noticing this, you may realize that the first element of each the first and the third parameters MUST be the same. If it's not, then the statement is not a true statement. So let's write the code that binds the first element to itself: append(List1,List2,List3) :- List1 = [First|Rest1], List3 = [First|Rest3]. This code by itself won't work correctly. All this code does is confirm that List1 and List3 begin with the same element. This is necessary in proving that List1 forms the beginning of List3, but it's not sufficient. Now we need to prove that the REST of List1 forms the beginning of the REST of List3. We can do this by calling append again, but with Rest1 and Rest3 in place of List1 and List3, like this: append(List1,List2,List3) :- List1 = [First|Rest1], List3 = [First|Rest3], append(Rest1,List2,Rest3). Notice that we are using "right recursion" here, because the append statement is placed "to the right" of all the other statements, and is called after all the variables have been bound together. Also notice that the second parameter still gets List2 passed in, because we will still need it in our final step of the proof. Now let's shorten the line above by substituting List1 and List3 to what they bind to: append([First|Rest1],List2,[First|Rest3] :- append(Rest1,List2,Rest3). So are we done? No, we still have to be able to prove a call to append as true so our recursive code can eventually prove to be true. By examining the code we just wrote, we can see that this line recurses until the first parameter no longer has any elements (because an empty list cannot bind to [First|Rest1]). So now what are we left with? Assuming we began with our example, List1 is now an empty list, List2 is the same as when we began, and List3 is now the original List3 but without List1's elements. We are now left with a call to: append([],[1,2,3],[1,2,3]). So how do we go about proving this statement as true? Well, the answer is quite simple. If the second parameter matches the third parameter (provided that the first parameter is an empty list), we can go ahead and state that it is a true statement, like this: append(List1,List2,List3) :- List1 = [], List2 = List3. But let's shorten this line by substituting, so we get: append([],List3,List3). Now you might recognize this line as being the "base case" in recursion. It won't matter with this particular example, but it's usually a good idea to place the base case above the recursive case. So, putting all the lines together, we get: % append example usage: append([a,b,c],[1,2,3],[a,b,c,1,2,3]). append([],List3,List3). append([First|Rest1],List2,[First|Rest3]) :- append(Rest1,List2,Rest3). That's our code for appending lists together. Now if we wanted to append the lists [1,2,3] and [4,5] together with the following line: append([1,2,3],[4,5]). we get the following error message: ++Error: Undefined predicate: append / 2 Aborting... Why did we get the error? Because we forgot to put in a variable as a third parameter. We should have called it like this: append([1,2,3],[4,5],AppendedList). There. That should work. ------------------------------------------------------------------- YET ANOTHER RECURSION EXAMPLE Let's do one last recursion example. Hopefully this example will help you to think around certain problems. Let's write code to reverse a list. The first thing we should do is acknowledge that we are not writing a function that taken a list as a parameter and returns the reversed list. Instead, we are writing a functor that takes both the normal list and the reversed list as parameters and states whether or not it can prove that the reversed list is indeed the reverse of the normal list. Next, let's write our comment line with an example usage: % reverse example usage: reverse([1,2,3,4,5],[5,4,3,2,1]). So, how do we begin? Well, by examining the example we can see that, naturally, the first element of the first list is the same as the last element of the second list. If those are able to bind, then we can call reverse again on the subsets of the lists: reverse(List1,List2) :- List1 = [First|Subset1], List2 = [Subset2|Last], First = Last, reverse(Subset1,Subset2). There. But wait! Notice the "|" operator when we are binding List2. This operator doesn't split out the last element from the rest of the list; it only splits out the first element! Therefore, that line will not work. Now you might be thinking to yourself, "Wouldn't it be nice if Prolog had an operator that split out the last element? It sure would save a lot of trouble." Now, think about it. As far as I know, Prolog does not have an operator that splits out the last element of a list, but we can write code that makes sure that the Last element is indeed the last element of List2. To do it, we use the append code: append(Subset2,[Last],List2). This code now guarantees that Last is the last element of List2. Notice that the variable named Last is placed inside a list, because append can only append lists together, not elements. Now let's replace that faulty line with our new one: reverse(List1,List2) :- List1 = [First|Subset1], append(Subset2,[Last],List2), First = Last, reverse(Subset1,Subset2). Now let's shorten the code by replacing the variables with what they bind to: reverse([First|Subset1],List2) :- append(Subset2,[First],List2), reverse(Subset1,Subset2). Notice that we can't replace List2 with anything because it can only bind using a call to append. Therefore, we have no choice but to leave it after the ":-" operator. But remember, to avoid left-recursion we will place it to the left of the recursive call. So now we have valid reversing code, but we still have a problem: when does it end? When do we finally declare that the subsets are reversed from each other? The answer is when they cannot possibly get any smaller... that is, when they are empty lists. So let's write the base case using empty lists. The reverse of an empty list is itself, right? So let's write that as a true statement: reverse([],[]). Now let's put all the lines of code together (remembering to place the base case above the recursive case): % reverse example usage: reverse([1,2,3,4,5],[5,4,3,2,1]). reverse([],[]). reverse([First|Subset1],List2) :- append(Subset2,[First],List2), reverse(Subset1,Subset2). Now you can test it with expressions like: reverse([a,b,c,d],ReversedList). reverse(OriginalSentence,[backwards,is,sentence,this]). Of course, for this code to work it requires that append be defined as well. Hopefully you should see that with useful little operations like append you can create more complex operations that we normally could not accomplish with what we originally had to work with. ------------------------------------------------------------------- TIPS Before I close, let me offer a few helpful hints: 1. Before writing code, acknowledge the fact that programming languages like C/C++, Java, and Lisp all return values. In Prolog, however, you will not return anything; if there is a value you want to discover you will pass a variable in its place in the parameter list. 2. Before typing out the code, write a comment line with an example usage, one that would make Prolog print "yes". 3. Once you have your comment line written out, refer to it as you try to figure out how you would go about proving that the example is correct. 4. When using recursion, remember to ALWAYS avoid left-recursion (when the recursive call is to the left of any other expressions) by placing the recursive call at the end of all the other expressions. 5. If reading Lisp code is too overwhelming for you, try expanding it by individually binding the parameters after the ":-" operator. Remember, though, that it's always best to shorten the code when you're writing it to save steps and computation time. ------------------------------------------------------------------- Well, that's about it. If you were able to read through this whole text file you hopefully have a deeper understanding (and maybe even a greater appreciation) for Prolog. Have fun programming! Jean-Luc Romano