ok@quintus (Richard A. O'Keefe) (07/03/88)
Yes, it's me again, with yet a third set of comments on the "Knowledge Systems and Prolog" book by Walker, McCord, Sowa, & Wilson. This particular set of comments pertains to chapter 3. I hasten to say at the outset that there is a lot of good stuff in this chapter which I wish I had written myself, but of course I have nothing to say about _that_. 1. Logic Programming Development Process (pp 110-111) Don't take steps 1..9 too seriously; that's not how I do it, and one of the most important steps has been omitted, namely "check to see if someone else has already solved the problem". But the rest of the advice on p111 is good. 2. Reading (p 113) The book presents two fragments (recast here as Prolog): ... read(Stream1, X), read(Stream2, T), process(X, T) ... and ... customer(Name), catalogue_item(Item), interested_in(Name, Item), send_letter(Name, Item) saying "For example, catalogue_item/1 may simply read the next term". Now the second fragment looks as though it has a declarative reading. But its behaviour is radically different from the behaviour which would result if customer/1 and catalogue_item/1 were tables. It is _NOT_ good style to give commands with side effects names which suggests that they are pure relations. In fact it is very bad style. Suppose we had the customer and catalogue_item tables in memory as pure relations, and wrote this failure-driven loop: send_letters :- customer(Customer), catalogue_item(Item), interested_in(Customer, Item), send_letter(Customer, Item), fail ; true. This would combine *every* Customer with *every* catalogue Item. But the original fragment with the two read commands doesn't do that; it reads an X and a T and combines _that_ X with _that_ T and no other. By the way, you _can_ keep (a small number of) tables in files and access them with read/1. Here's an example (using Quintus Prolog): :- dynamic table_stream/2. initial_position('$stream_position'(0,1,0)). read_from_external_relation_1(Table, Entry) :- ( table_stream(Table, Stream) -> true ; external_relation(Table, FileName), open(FileName, read, Stream), assert(table_stream(Table, Stream)) ), initial_position(Zero), do_external_access(Zero, Stream, Entry). do_external_access(Before, Stream, Entry) :- stream_position(Stream, _, Before), read(Stream, Term), stream_position(Stream, After), ( Term == end_of_file -> fail ; Entry = Term ; do_external_access(After, Stream, Entry) ). external_relation(customer, 'custom.dat'). external_relation(catalogue_item, 'catalo.dat'). customer(Customer) :- read_from_external_relation_1(customer, Customer). catalogue_item(Item) :- read_from_external_relation_1(catalogue_item, Item). Now, this _is_ a version of catalogue_item/1 which "simply read[s] the next term", but it is pretty remote from the first fragement. I don't know whether Frank McCabe invented this technique, but he's the one I got it from (testing the code above was the first time I have ever _used_ the technique, mind...). If you can possibly fit the information into memory, _do_ it. Don't keep reading it from a file over and over again. Another possibility, instead of using read_from_external_relation_1/2, would be to read a file once and build an index in memory, so that fewer reads would be done. For example, suppose we have a a file 'passwd.pl' containing items like passwd(teksup,123,45,'Tech Support', '/peano/teksup','/bin/csh'). passwd(halt, 6, 7,'Stop Machines', '/', '/etc/halt'). passwd(ok, 89,10,'Richard A. O''Keefe','/dedekind/ok', '/bin/csh'). {Crackers beware: these are _not_ real Quintus /etc/passwd entries!} We might do this: :- dynamic passwd_stream/1, passwd_index/2. initialise_passwd(Stream) :- open('passwd.pl', read, Stream), assert(passwd_stream(Stream)), repeat, stream_position(Stream, Position), read(Stream, Term), ( Term = passwd(Key,_,_,_,_,_) -> assert(passwd_index(Key, Position)), fail ; true ), !. % The Stream is left open! passwd1(Key, Uid, Gid, Name, Home, Shell) :- ( passwd_stream(Stream) -> true ; initialise_passwd(Stream) ), passwd_index(Key, Position), stream_position(Stream, _, Position), read(Stream, passwd(Key,Uid,Gid,Name,Home,Shell)). It is fair to give a predicate so defined a name which suggests that it is a pure table, because (apart from tying up a stream and being rather slow) that's how it _acts_. ONLY use this technique if you can build a good index; it can be hundreds, even thousands, of times slower than accessing tables in main memory. 3. Wrappers (page 114-115) There is an example on pp 114-115 which reads thus: name(Name, TelephoneNumber, File) :- get_next_record(File, Record), parse_record(Record, name(Name,TelephoneNumber)). ^^^^^ ^ parse_record(Record, name(Name,TelephoneNumber)) :- ^^^^^ ^ parse_name(Name, Record, Rest_of_record), parse_blanks(_, Rest_of_record, Last_of_record), parse_telephone(TelephoneNumber, Last_of_record, _). {If you look at the code in the book, you'll see that the first argument of parse_blanks/3 is an anonymous variable in all calls and definitions, so it might as well not be there.} There is much to quarrel with in the choice of names here: the parse_*** predicates are genuinely relational, so should have noun-phrase or adjective-phrase names, not names that look like commands. Worst of all, the operation which _is_ a command (that is, which has a side effect) is given the name name/3 which looks like a pure relation. It would be better named e.g. read_name_and_phone_number(File, Name, PhoneNumber) My main point here is that name(_,_) is a useless wrapper. If the name and phone number had to be returned to the caller so packaged, there'd be some sense in it, but not here. It would be better as read_name_and_phone_number(Stream, Name, PhoneNumber) :- get_line(Stream, Chars), name_and_phone_number(Name, PhoneNumber, Chars, _). name_and_phone_number(Name, Exchange-Extension) --> token(Name), blanks, number(Exchange), "-", number(Extension). where the compound term -(_,_) here _is_ justified because that's what the caller wants. 4. Query-the-user (pp 116-117, p120). When you hit page 116, look ahead to page 120. I'll not comment on the rather major problems of the code on page 116, because the code on page 120 remedies some of them. The idea is that you want a relation user('User''s first name') -- by the way, I find it offensive to have computers address me by my first name, so whenever a program asks me my first name I lie; my computer account name will do fine for any genuine identification-- but you don't want to ask unless you turn out to need the information. The code on page 120 is (converted to Quintus Prolog): :- dynamic known/1. user(Name) :- known(user(Name)), !. user(Name) :- prompted_line('What is your first name? ', Chars), ( verify(Chars) -> % you have to supply verify/1 atom_chars(Name, Chars), assert(known(user(Name))) ; format('~s: not verified.~n', [Chars]), user(Name) ). {This is not identical to the code in the book, but it has the same kinds of bugs, which is what I'm concerned with.} I loaded this into Quintus Prolog, together with library(prompt) and this definition of verify/1: verify([_,_]). Now see what happens: | ?- user(fred). What is your first name? jim jim: not verified. % right What is your first name? ok no % right, BUT | ?- listing(known/1). % prints _nothing_ | ?- user(Who). % should pick up 'ok', BUT What is your first name? ok % it asks AGAIN! Who = ok % but that's not all... | ?- user(dz). What is your first name? ok % it asks yet AGAIN! no The code breaks down in (at least) two ways: A) If we call it with Name bound when known(user(X)) is true--i.e. the user has been asked for his name--but X\==Name, the user will be asked for his name again. Fortunately, the _second_ breakdown then occurs, so at least it isn't _stored_ again. B) If we call it with Name bound when the user name is not known (or when it is known to be different from Name), we'll ask for the name, verify it, and then fail before storing the name. How should it be written? :- dynamic property_known/2. property_type(user, atom). ... property_prompt(user, 'What is your first name? '). ... property_verify(user, Chars) :- verify_user(Chars). ... simple_query(Property, Value) :- simple_query_1(Property, X), Value = X. simple_query_1(Property, Value) :- property_known(Property, Value), !. simple_query_1(Property, Value) :- property_type(Property, Type), simple_query_1(Type, Property, Value), assert(property_known(Property, Value)). ... simple_query_1(atom, Property, Value) :- property_prompt(Property, Prompt), repeat, prompted_line(Prompt, Chars), ( property_verify(Property, Chars) ; format('~s: not verified~n', [Chars]), fail ), !, atom_chars(Value, Chars). ... user(UserName) :- simple_query(user, UserName). Now, with this definition, we get | ?- user(fred). What is your first name? jim jim: not verified What is your first name? ok no | ?- listing(property_known/2). property_known(user,ok). yes | ?- user(Who). Who = ok | ?- user(dz). no which is what you would expect something like this to do. 5. "Global variables" (p122) I have to quote their VM/Prolog code here, because delax/1 doesn't behave quite like retract/1, more like retract_first/1. A ::= B <- try(delax(global_value(A, *))) & addax(global_value(A, B)). try(X) <- X. try(*). What's wrong with this? Well, after converting to Quintus Prolog, I got the following result: | ?- x ::= 1, listing(global_value/2). global_value(x,1). | ?- x ::= 2, global_value(x, 1). no | ?- listing(global_value/2). global_value(x, 2). global_value(x, 2). I'll leave you to figure _that_ one out for yourselves, but the moral is that any time the last clause of a predicate is a catch-all case like the last clause of try/1, the chances are someone is trying to be clever and failing. 6. Another steadfastness problem (p122) We are told on page 122 that "the global_value/2 technique described above can be used to simulate a stack, as follows: push(Stack, Item) <- addax(global_value(Stack, Item), 0). % "asserta" pop(Stack, Item) <- delax(global_value(Stack, Item)). The predicates push/2 and pop/2 have their obvious meaning." Actually, they haven't. Or at any rate pop/2 hasn't. If pop/2 succeeds, it did delete an item from the stack, but the item it deleted may not have been at the top of the stack... Working Prolog code is push(Stack, Item) :- asserta(global_value(Stack,Item)). pop(Stack, Item) :- retract(global_value(Stack,Top)), !, Item = Top. top(Stack, Item) :- global_value(Stack, Top), !, Item = Top. TO BE CONTINUED I have about as much more to add, but it's time to go home. DISCLAIMER Remember, I'm criticising slips in a by-and-large-reasonable book; the good stuff can speak for itself. You'll recall that I had very little to criticise in the _content_ of the Warren & Maier book, but raged about the misleading preface and blurb. I recommended it to the Prolog class I taught last week, and if I had bought the Walker et al book by then I'd probably have suggested it to them as worth reading too. Yes, I've made up my mind, I _shall_ recommend it to the next Quintus class, if only to show them why they should buy QP370 rather than VM/Prolog (:-).