🚀 go-pugleaf

RetroBBS NetNews Server

Inspired by RockSolid Light RIP Retro Guy

Article View: comp.lang.prolog
Article #193

Re: debugging psychologist program.

#193
From: Richard A. O'Keefe
Date: Tue, 14 Apr 1992 05:24
440 lines
13966 bytes
In article <2476@loki.une.edu.au>, peter@loki.une.edu.au (Peter Radonyi) writes:
> ===========================  Start of program Matilda =======================
> /*
>  The program is a big loop, getting a response from the user
>  an 
> d branching according to the user input. 
> */
> 

>   write('Gooday'),
>   nl,
>   write('Would you like to discuss your problem?'),
>   nl,
>   continue.                                  % initialisation.

Why the newlines?  Why the English problem?  It's "Good-day", or, if like
me you're from the Southern Hemisphere, "G'day" or possibly even "Dayee".
But never "Gooday".

A point here is that if a program asks me a yes/no question, I expect it
to be prepared to handle a yes/no answer.  In this case, if I type back
"no", I expect the program to have the good manners to go away.  Either
do that or don't set up the expectation.  Here's a good start:

	matilda :-
		write('Good-day.  Please tell me about your problem.'), nl,
		continue.

> continue :-                                  % main loop
>   finished,                                  % exit flag
>   tidyup ; 
>   write('> ') , 
>   getaphrase(Yourmessage) ,             
>   respondTo(Yourmessage) , 
>   continue.

The layout convention here is _guaranteed_ to cause confusion.  Adopt
this simple rule:  ALWAYS put semicolons at the BEGINNING of lines.
It took me a long time to notice the semicolon after tidyup.  Also, I
went looking for the predicate 'finished'/0, and couldn't find it.  In
several Prologs, you would _have_ to declare dynamically modified
predicates.  In Prologs which don't support the ?-dynamic declaration,
it is a good idea to put it in _anyway_, so that human readers don't
go on a wild-goose chase.

What's more, this should not be an OR;ELSE.  If finished/0 is true, 
you don't want to do the write,getaphrase+... stuff at all.  You
want an IF->THEN;ELSE.  So write one.  This particular problem was
pervasive in the original code.  A better version:

	/* ?- dynamic(finished/0). */

	continue :-
	    (	finished ->
		tidy_up
	    ;	write('> '),
		get_a_phrase(YourMessage),
		respond_to(YourMessage),
		continue
	    ).

This, by the way, is a rather clumsy way to construct the loop.
Consider what happens if someone tries to run matilda again; the
'finished' fact will still be there, so the second time it will
just stop.  Here's how to do something like this.

	continue :-
		write('> '),
		get_a_phrase(YourMessage),	
		respond_to(YourMessage).

The clauses for respond_to/1 that want to keep going just call
continue.  The clauses for respond_to/1 that want to stop just
call tidy_up.  I'll assume that from now on.

Salvaging getaphrase/1 would be more work than just picking some
something workable from a textbook.

Let's come up with a specification.

    As far as get_a_phrase/1 is concerned, characters are
	- letters a..z A..Z
	- digits 0..9
	- punctuation ,;:?!.
	- end of line
	- other
    get_a_phrase/1 is to return a list of all the tokens on the
    next input line, where a token is
	- a block of letters, returned as an atom
	- a block of digits, returned as an integer
	- a single punctuation mark, returned as an atom

So we start by building a table:

	character( 0, other).
	...
	character(13, end).
	...
	character(32, other).
	...
Doing this by hand is rather unpleasant.  A better way is to write
a predicate:

	write_character(Char, Class) :-
		write(character(Char,Class)), write(.), nl, fail.

	make_table :-
		EndOfLine = 13,	% use 10 in UNIX systems
		(   write_character(EndOfLine, end)
		;   member(X, "abcdefghijklmnopqrstuvwxyz"),
		    write_character(X, letter)
		;   member(X, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
		    write_character(X, letter)
		;   member(X, "0123456789"),
		    write_character(X, digit)
		;   member(X, ",;:?!."),
		    write_character(X, punct)
		;   member(A, [ 0, 1, 2, 3, 4, 5, 6, 7]),
		    member(B, [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15]),
		    X is A*16 + B,
		    X =\= EndOfLine,
		    \+ member(X, "abcdefghijklmnopqrstuvwxyz"),
		    \+ member(X, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
		    \+ member(X, "0123456789"),
		    \+ member(X, ",;:?!."),
		    write_character(X, other)
		;   true
		).

	make_table(File) :-
		telling(OldOutput),
		tell(File),
		make_table,
		told,
		tell(OldOutput).

Now:

	get_a_phrase(Phrase) :-
		rest_of_phrase(Tokens),
		Phrase = Tokens.

	rest_of_phrase(Tokens) :-
		get0(Char),
		character(Char, Class),
		rest_of_phrase(Class, Char, Tokens).

	rest_of_phrase(other, _, Tokens) :-
		rest_of_phrase(Tokens).
	rest_of_phrase(end, _, []).		
	rest_of_phrase(punct, Char, [Token|Tokens]) :-
		name(Token, [Char]),	% use atom_chars/2 if possible
		rest_of_phrase(Tokens).
	rest_of_phrase(letter, Char, Tokens) :-
		rest_of_word(Char, Chars, Chars, Tokens).
	rest_of_phrase(digit, Char, Tokens) :-
		rest_of_number(Char, Chars, Chars, Tokens).

	rest_of_number(Char, [Char|Chars], String, Tokens) :-
		get0(Next),
		character(Next, Class),
		rest_of_number(Class, Next, Chars, String, Tokens).

	rest_of_number(digit, Char, Chars, String, Tokens) :- !,
		rest_of_number(Char, Chars, String, Tokens).
	rest_of_number(Class, Char, [], String, [Token|Tokens]) :-
		name(Token, String),	% use number_chars/2 if possible
		rest_of_phrase(Class, Char, Tokens).

	rest_of_word(Char, [Char|Chars], String, Tokens) :-
		get0(Next),
		character(Next, Class),
		rest_of_word(Class, Next, Chars, String, Tokens).

	rest_of_word(letter, Char, Chars, String, Tokens) :- !,
		rest_of_word(Char, Chars, String, Tokens).
	rest_of_word(Class, Char, [], String, [Token|Tokens]) :-
		name(Token, String),	% use atom_chars/2 if possible
		rest_of_phrase(Class, Char, Tokens).

> /* 
> Branch acccording to the different responses.
>  1 no response
>  2 one word response
>  3 two word response
>  4 response to a swear word
>  5 respond to an exit bye 
>  6 responding to a key word
>  7 responding to a key phrase
>  8 recalling an earlier point.
>  9 IF ALL FAILS a CATCH ALL "Please tell me more ..."
> The order above will not work,  because it responds to a one
> word response before it looks at exit or bye. The problem with
> this is that bye and one letter swear words will never be
> instantiated because the program matches them in the first. A
> trap for young players.
> Correct order is
>  1 no response
>  2 respond to an exit "bye"
>  3 respond to a swear word
>  4 one word response
>  5 two word response
>  6 responding to a key word
>  7 responding to a key phrase
>  8 recalling an earlier point.
>  9 IF ALL FAILS a CATCH ALL "Please tell me more ..."
> 
> */

> 
> % 1 response to a no response.
> respondTo([]) :- 
>      write('Say something please.'),
>      nl.

> % 2 exit line
>      respondTo(["bye"|_]) :-
>      write('Thankyou for a very interesting talk.'), nl,
>      write('I hope we will talk again, farewell for now ...'),
>      nl,
>      assert(finished).

Note that there are no cuts here!  The patterns in the heads of the
clauses are not sufficient to determine a unique match.  You need
cuts here (and you explicitly want the ordered if-then-else effect
that you'll get).

	respond_to([]) :- !,
		write('Please say something.'),
		continue.

	respond_to([bye|_]) :- !,
		write('Thank you for a very interesting talk.'), nl
		write('I hope we shall talk again.  Good-bye for now.'), nl,
		tidy_up.

	respond_to(Phrase) :- 
		means(Word, profanity),
		member(Word, Phrase),
		!,
		write('Please do not use words like '),
		write(Word), write('.'), nl,
		continue.

	respond_to([_]) :- !,
		write('Please do not be so short with me.'), nl,
		continue.

	respond_to([_,_]) :- !,
		write('That is not much to go on.'), nl,
		continue.

	respond_to(Phrase) :-
		member(Word, Phrase),
		means(Word, Meaning),
		response(Meaning, Word, Response),
		!,
		print_phrase(Response),
		asserta(memorised(Phrase)),
		continue.

Argh!  Another unnanounced dynamic predicate.  I'll interrupt respond_to/1
to show the revised top of the program:

	/* ?- dynamic(memorised/1) */

	matilda :-
		retractall(memorise(_)),
		write('Good-day.  Please tell me about your problem.'), nl,
		continue.
 
LPA Prolog has retractall/1, so that shouldn't be any problem.
Now tidy_up/1 disappears.  Let's resume respond_to/1.
		
	respond_to(Phrase) :-
		dissect(Phrase, First, Subphrase, Second),
		subphrase(Pattern, Meaning),
		construct(First, Meaning, Second, Response),
		!,
		print_phrase(Response),
		continue.

	respond_to(_) :-
		retract(memorised(Phrase)),
		!,
		write('You said that '),
		print_phrase(Phrase),
		continue.

	respond_to(_) :-
		write('Please tell me more.'), nl,
		continue.

Let's make these things a wee bit more direct.  Instead of
	dissect(Phrase, First, Subphrase, Second),
	subphrase(Pattern, Meaning),
let's have one predicate to do it.  For each original
	subphrase([W1,W2,...,Wn], Meaning)
clause, we'll have a clause
	subphrase([W1,W2,...,Wn|Rest], [], Rest, Meaning)
and we'll have a looping clause

	subphrase([W|Ws], [W|Front], Rest, Meaning) :-
		subphrase(Ws, Front, Rest, Meaning).

We get
	subphrase([i,feel|Rest],   [], Rest, i_feel).
	subphrase([hates,me|Rest], [], Rest, hates_me).
	subphrase([W|Ws],   [W|Front], Rest, Meaning) :-
		subphrase(Ws, Front, Rest, Meaning).

	construct(i_feel, _, Rest,
		[do,you,often,feel|Rest]).
	construct(hates_me, Front, _,
		[how,do,you,relate,to|Front]).

By the way, I don't understand a question "how do you relate to so-and-so?".
I honestly don't know what it means, and I'm a native speaker of English.
I guess it means "how are you related to so-and-so?" with answers like
son/cousin/foreman/secretary/...  But that's a conjecture.

The clause that uses "subphrase" becomes

	respond_to(Phrase) :-
		subphrase(Phrase, Front, Rest, Meaning),
		construct(Meaning, Front, Rest, Response),
		!,
		print_phrase(Response),
		continue.

It's better to put "key" arguments first in a predicate, so

	respond(family, Word,
		[tell,me,more,about,your,Word]).
	respond(worry, Word,
		[are,you,doing,anything,about,the,Word,?]).

> % I don't understand the next line
> '?ERROR?'(2,_) :-
> !, fail.

Looking in the index of the LPA Prolog manual is always a good idea.
In my copy, it says on p383
	"?ERROR?/2, user defined error handler
		244, 253, 343"

Page 244 has sections 24.1.1 "Handling Numbered Errors" and
24.1.2 "Writing a User Defined Error Handler".  Reading the
section in question, we find on page 245 an example involving
error number 2, which starts out like this:

	'?ERROR?'(2, Call) :- % error 2 is pred not defined

What's it doing?  Well, you have a predicate memorised/1 which
sometimes has no clauses in it.  Many other Prologs can tell the
difference between a defined predicate that happens to have no
clauses at the moment (because there is a ?-dynamic declaration
for it) and a predicate which has no clauses because it has
never been defined (because there is no declaration for it).
The version of LPA Prolog you are using can't.  So the undefined
predicate error is being switched off.

Here is a better approach, adapted from p245 of the LPA manual.
(You get a free bug fix.  How about that?)

	/* ?- dynamic(declared_as_dynamic/1). */

	dynamic(Name/Arity) :-
		functor(Call, Name, Arity),
		(   declared_as_dynamic(Call)
		;   assert(declared_as_dynamic(Call))
		),
		!.

	'?ERROR?'(2, Call) :-
		(   Call = declared_as_dynamic(_)
		;   declared_as_dynamic(Call)
		),
		!,
		fail.
	'?ERROR?'(No, Call) :-
		errm(No, Msg),
		fw('WND:', [q(0),u(0),u(0)], [Call,' caused error ',Msg]),
		abort.

Now calling ?-dynamic(memorised/1). will tell LPA Prolog to shut up and
fail quietly if you call memorised(_) when it happens to have no clauses,
but to keep on reporting as errors calls to predicates that have no
clauses and have not been declared as dynamic.

Better still, rewrite the code so that it passes the stack of phrases
around as an argument.

	print_phrase([]) :-
		nl.
	phrase_phrase([Word|Words]) :-
		write(Word),
		print_rest_phrase(Words).

	print_rest_phrase([]) :-
		nl.
	print_rest_phrase([Word|Words]) :-
		punctuation(Word, Class),
		!,
		write(Word),
		print_after_punctuation(Words, Class).
	print_rest_phrase([Word|Words]) :-
		put(32),
		write(Word),
		print_rest_phrase(Words).

	print_after_punctuation([], _) :-
		nl.
	print_after_punctuation(stop, Words) :-
		put(32),
		print_rest_phrase(Words).
	print_after_punctuation(pause, Words) :-
		print_rest_phrase(Words).

	punctuation(',', pause).
	punctuation(';', pause).
	punctuation(':', pause).
	punctuation('?', stop).
	punctuation('!', stop).
	punctuation('.', stop).


> The next program is a modified version of the above program reading in the
> characters as a string of characters rather than a list.

Er, a string of characters *IS* a list in LPA Prolog.

> Thanks for any light you can throw on these two programs.

The revised code above works with SICStus Prolog (if you add a suitable
definition of member/2).
-- 
I am writing a book on debugging aimed at 1st & 2nd year CS students using
C/Modula/Pascal-like languages.  Please send suggestions (other than "you
_must_ cite "C Traps and Pitfalls") to ok@goanna.cs.rmit.oz.au
Technical Details
Message-ID: <10236@goanna.cs.rmit.oz.au>
Path: arc1-ca.pugleaf.net!sparky!uunet!munnari.oz.au!bruce!goanna!ok
References: <2476@loki.une.edu.au>

Show Full Headers
Complete Article Headers
Path: arc1-ca.pugleaf.net!sparky!uunet!munnari.oz.au!bruce!goanna!ok
From: ok@goanna.cs.rmit.oz.au (Richard A. O'Keefe)
Newsgroups: comp.lang.prolog
Subject: Re: debugging psychologist program.
Keywords: prolog
Message-ID: <10236@goanna.cs.rmit.oz.au>
Date: 14 Apr 92 05:24:18 GMT
References: <2476@loki.une.edu.au>
Organization: Comp Sci, RMIT, Melbourne, Australia
Lines: 440