~1 LOGIC PROGRAMMING LESSON 8 In this lesson, I shall combine the Prolog control language (Lesson 7) and the logic language (Lessons 1 to 6). This will enable you to write command predicates that vary their action according to their arguments. ~2 Let's look at a simple example. Here is a command predicate that takes a number as argument, and writes a message saying how big it is. write_size(N) :- N < 3, !, write(tiny). write_size(N) :- N < 10, !, write(small). write_size(N) :- N < 100, !, write(medium). write_size(N) :- write(big). There's a copy of this in knowledge base SIZE. Restore it, and try it out on a few numbers. ~3 Each clause of this predicate (except the final one) consists of three goals. Remember from Lesson 4 that a GOAL is what we call one single use of a predicate in the tail of a clause. The first goal is a test, expressed in Prolog's logic language. It uses arithmetic comparisons, introduced in Lesson 5. The second goal is the special symbol !. In speech and English writing, this is always called "cut", but you must write it in programs as !. Don't confuse it with the stick-symbol | . The third goal is a command: the sort of thing we dealt with in Lesson 7. How does this translate into English? ~4 Altogether, each clause (except the last) can be translated thus: To write the size of N: if N < 3 then write "tiny" otherwise go on to the next clause. The last clause takes effect if none of the comparisons in the others succeed, and it can be translated as To write the size of N: (none of the tests in the clauses above succeeded) write "big" ~5 So we are defining a command predicate, but a special one where the internal command is only obeyed if the argument passes a previous test. It's important to realise that this works because Prolog scans clauses from the top of the database downwards. If it started at the bottom and worked up, or picked a clause at random, the effect would be different, because the tests are not mutually exclusive. For example, what would be the effect if Prolog started at the bottom and worked up? Clearly it would be the same as if _our_ clauses were in reverse order with Prolog scanning downwards, as it does. By using "copy" and "delete" you can move clauses around. If you're doubtful that their order matters, try re-ordering them and then calling "write_size". This should convince you that when writing these command predicates, you have to take clause order into account. There are some languages whose programs would work just as well if you punched them on cards and threw the cards at random into the computer. This is not true of Prolog! ~6 Going back to the translation, you can see that I'm using the cut as a kind of "if". Don't confuse this kind of "if" with :- . The :- "if" is used only to connect a predicate you're defining with the goals that make up its tail. It is not a goal, and it can't appear in the tail of such a predicate. The ! "if" is a special kind of goal that has the function of interfacing between tests (in the logic language) and commands (in the control language). It can only appear in the tail of a predicate. ~7 There is actually more to the cut than this. What I'm doing is presenting a very simplified story; "taming" the cut by showing you a particular pattern of usage. Understanding the cut in full is rather complicated, and it's best to take it a bit at a time. If you did the exercise at the end of Supplement 5, you will realise that this is so: please bear with me until the end of this lesson. ~8 Now back to our example, which you should have already restored. In general, how do you translate such a thing into English? Well, the test (before the cut) is in Prolog's logic language, so you translate it as described in Lesson 4. The commands (after the cut) are in Prolog's control language. Translate them as in Lesson 6. Then join the two by an "if". Finally, translate the head of the predicate as a command predicate ("to do something..."). Repeat this for each clause. ~9 This will give you To write the size of N: If N is less than 3, then write "tiny". To write the size of N: If N is less than 10, then write "small". To write the size of N: If N is less than 100, then write "medium". To write the size of N: Otherwise, write "big". ~10 For a bit of practice: modify "write_size" so that it writes out "enormous" if N is >= 100000, and "minuscule" if it's less than 0.00005. ~11 Here is another example, based on the "mars" knowledge base of Lesson 5, the one with sweet prices. Type in these clauses for "describe": describe(S) :- costs( S, P ), P < 10, !, write( bargain ). describe(S) :- costs( S, P ), P < 18, !, write( average ). describe(S) :- costs( S, P ), P < 100, !, write(expensive). describe(S) :- write('only for millionaires'). ~12 Now restore mars, and try calling "describe". How does it translate to English? As for "write_size", the test is translated as in the logic language. The commands (write) are translated as in the control language. So the first clause becomes: To describe S: If S costs P, and P is less than 10, then write "bargain". which means To describe S: If S costs less than 10p, then write "bargain". ~13 Incidentally, you may have tried "analyse" on these facts. Don't get confused. Analyse can't distinguish between the logic and control languages, and it treats everything as being in the logic language. So it will not really help you translate anything with commands in. ~14 Now, please restore knowledge base "links". This contains facts similar to those for the connections in Traveller's board, such as goes_to( a, b ). goes_to( b, c ). goes_to( c, d ). Unlike the connections in Traveller, these are one-way links, so each square has one successor: a has successor b, which has successor c. Now, can you write a command predicate "look(X)". X is the name of a square, such as a or b. "look" must write "goes to c" or "goes to d" (e.g.) if X has a successor, and "dead end" if it doesn't. So the message for a square with successor contains the name of the successor. ~15 If in doubt, think up the English translation and work backwards. You want the clauses to do the following: To look at X: if X has successor Y, then write "goes to" and Y. To look at X: (otherwise) write "dead end". ~16 Hint: look( X ) :- ... , !, write( '...' ), write ( ... ). look( X ) :- write( '...' ). ~17 The answer will be something like look( X ) :- goes_to( X, Y ), !, write( 'Goes to ' ), write ( Y ). look( X ) :- write( 'Dead end' ). ~18 "look" is itself a command predicate, so it can be called from the command part of a command predicate. So: what trivial alteration to "look" will enable it, having reported a "goes to", to then look at _that_ square, and so on until it runs off the end of the sequence? ~19 Hint: use recursion in clause 1. ~20 Here's the answer, with an "nl" added to separate the messages. look( X ) :- goes_to( X, Y ), !, write( 'Goes to ' ), write ( Y ), nl, look( Y ). look( X ) :- write( 'Dead end' ). ~21 You can now, at last, get an idea of how Prolog can be used for data navigation, both around abstract structures like those of the story and depression examples in Supplement 4, and around the very concrete structures describing Traveller. And since you already know how to add and delete facts using "add" etc, you can even see how the world-simulator in Traveller can run a lorry round the board, calling your logic predicate "act" at each stage, examining its actions, and updating the world accordingly. ~22 Before leaving this lesson, there are two final topics. Find or re-type your version of "write_size". Alter it so that if the number N is two or three, "write_size" writes out its name, otherwise writes an estimation of the size as before. ~23 You do this in exactly the same way, but since you want to test for exact equality with a number, you use = instead of < in these new clauses. write_size( N ) :- N = 2, !, write( two ). ~24 There is a convenient abbreviated form of such clauses you can use in such cases: write_size( 2 ) :- !, write( two ). It would translate as: To write the size of 2: (no extra conditions) then write "two". ~25 In general, if you want to write a clause which takes some action, but only if given one particular constant, you can write the constant in the appropriate argument, instead of using an = test. This is what I did with write_size(2), above. ~26 Now, before ending this lesson, the truth! There are not two languages but one. Prolog recognises no distinction between the control language and logic language (which is why "analyse" can't do so either). The distinction is, though, a convenient way of thinking about programs. Some predicates are obeyed for their side-effects: write, nl, adda, look. Many are obeyed for their logical result: costs, >, loves. You can mix the two indiscriminately and Prolog will never complain. However, programs in which this is done are hard to understand, and it is a good idea (at least if you're new to Prolog) to follow a few standard patterns when you write code. ~27 During these lessons, I have introduced three such patterns. 1) Predicates whose tails contain only logical predicates. Obviously such a predicate can itself never have side-effects (can not be a command predicate), and is thus a logical predicate. 2) Predicates whose tails contain only command predicates. Obviously such a predicate is itself a command predicate. 3) Because commands are no good if they can't adapt to their environment, we need to write command predicates that can be guided by logical tests. There are various ways to do this: but the command :- test1, !, action1. command :- test2, !, action2. ... command :- action_n. formula is adequate for almost all purposes, and results in a nice easy-to-read style ~28 In fact, _any_ predicate can be understood both as if in the logic language and as if in the control language. In the logic language, command predicates like "write" almost always turn out to have the logical result "true". In the control language, logic predicates do not have side-effects (they don't change their environment). Both are related by Prolog's order of execution. Because it scans the database from top to bottom, you can understand a sequence like command :- test1, !, action1. command :- test2, !, action2. command :- action_n. as a sequence of if-then-elses. Because it works through goals from left to right, you can understand a, b, c as meaning "do a; then do b; then do c". ~29 Since there is no difference between logic and control language, the cut can't be an interface between the two. That was a convenient fiction that ties in with my recommended programming style. The command predicates in this lesson would all work after a fashion if the cuts were removed. However, they would produce spurious answers or output if suitably provoked by backtracking, mentioned at the end of Lesson 5. If you did the experiments with cut at the end of Supplement 5, you might now like to carry them further. If not, you can ignore the final sections of this lesson, and just follow the rule that, in writing a command predicate, you should always put a cut between the test and the action. ~30 OK - I assume you are willing to experiment further with cut. Please restore SIZE (so you have the original) and ask write_size( 1 ), 1=2? Now edit the definition so as to remove all the cuts, giving you clauses like write_size(N) :- N < 3, write(tiny). and ask again write_size( 1 ), 1=2? ~31 Why do you think this happens? Essentially, the cut inhibits Prolog's clause scanning and says "once this clause has succeeded, never try an alternative". It is certainly undesirable that a command predicate should behave like this; the point of such a predicate is that each condition has one and only one correct action. If you don't backtrack into cut-less command predicates, they will behave as expected. But if you do, they won't. So for safety, I always use cuts. They also act as a good psychological cue signalling where the test ends and the action begins.