[comp.lang.prolog] "Knowledge Systems and Prolog", chapter 3.

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 (:-).