Thursday, July 22, 2010

Porting the Amateur Reasoner

I ported my Amateur Reasoner program, which I mentioned in my previous blog, to Delphi. Apart from a few specific points, this was a fairly simple process, and now that all the display code has been stripped from the program, it's much easier to understand the program source.

Before going any further, I should describe the program and what it does. First, I'll quote a very simple database which is dated 10 Feb 1989:

goal (rate)

if day = saturday then rate = cheap.
if hour = before_8:30 then rate = cheap.
if hour = after_21:00 then rate = cheap.
if day = sun_to_thurs & hour = 8:30_thru_13:00 then rate = expensive.
if day = sun_to_thurs & hour = 13:00_thru_21:00 then rate = middle.
if day = friday & hour = 8:30_thru_13:00 then rate = expensive.
if day = friday & hour = 13:00_thru_21:00 then rate = cheap.

prompt (day) = On which day was the call made?
prompt (hour) = At which hour was the call made?

The goal of the program is to 'calculate' or infer what the rate should be made for a telephone call according to its rules. In order to solve the goal, the program looks at the various rules which define the rate (this program is exceedingly simple, because it has a 'depth' of 1 - all rules directly refer to 'rate'). In order to infer what the rate is, the program has to determine on which day and at which hour the call was made - these are the prompts.

The first design change was to use variable length strings as opposed to fixed length strings. The only reason that I can think of why the original program used fixed length strings if it compiled with Turbo Pascal 5 is that the program must have originated on the PDP, whose Pascal had fixed length strings. Using variable length strings had slightly more consequences than I had considered: the program's parser, in common with all other programming language parsers, has to tokenise the input file, ie split the text into words and punctuation. The tokenisation process changed slightly after I redefined what a token was; this may or may not have been the cause of a bug which I found in the tokeniser procedure, which affected the parsing of a punctuation symbol at the end of a valid token.

In order to save memory, all the tokens were stored in a hash table and the data structures used the hash numbers instead of the actual tokens. The program had used a simple hash function which depended on the first three characters of the token, which is possible if the token has a fixed length but would return variable results if the token had less than three characters. As one keyword is 'if', it was clear that the hash function had to be replaced. Fortunately I discovered that Delphi has a THashedStringList class which solved all of my hashing problems in one go, so I immediately adopted this class.

I tested the parser initially on a 'database' called 'Actuary', which consisted of several rules and questions which would determine a person's life expectancy. The parser always choked on this file and I discovered eventually that it was using a keyword undefined in the parser. I then checked this database on the original program which also got stuck, so I realised that this extra keyword was in fact superfluous. When I removed all the offending statements, I ran the database through the program which worked fine. After thinking about it (and checking the dates on the files), I realised that this database had been intended for an earlier version of the Reasoner, and that later versions were able to establish for themselves the values needed (using the above example, the program is able to establish that valid values for 'day' are saturday, sun_to_thurs and friday).

The only other part of the program which gave me a few problems was where the program had to get input from the user - to establish what the values of 'day' and 'hour' should be. This is the part of the original program which was chock full of display code trying to emulate a GUI in a text only environment. Of course, all this code was irrelevant, but I had to be careful that I didn't 'throw the baby out with the bathwater'. As it happens, I did throw out a little too much code.... The part which determined which tokens to display on the screen (saturday, sun_to_thurs and friday) was straightforward, but what I missed the first time round was the value which would be returned after one of the tokens was chosen. I had passed the values to a radiobox and originally returned the index of the chosen token, which in this case would be 0, 1 or 2. No! The value to be returned should be the hash value of the chosen token. Whilst it occurs to me now that I could have recalculated the hash value, I already knew the hash value of each token as I accessed it when populating the radio group. Here is the Delphi screen


I remembered the little code trick which I presented in a column a few days ago about storing an integer along with a string value in a combobox. Here I used it with a radiogroup -
 ques:= curobj^.legal;   // this is a pointer to the list of values
 while ques <> nil do
  begin
   rg.Items.AddObject (hashstrings[ques^.n], TObject (ques^.n));
   ques:= ques^.next
  end;

 if showmodal = mrOK
  then with rg do
   result:= longint (items.Objects[itemindex]);

One neat thing which I added to the dialog box was the ability to see the program's reasoning. This existed in the original program too, so I wasn't inventing anything, but when I initially planned how to display this dialog, I thought that pressing on the 'do you want to see my reasoning' button would have brought up another dialog. In the end, I decided to extend the current dialog box downwards in order to reveal another memobox. Below is the screenshot of the same dialog box after the user has clicked on the 'do you want to see' button.


Now that I have the program and got it running with Delphi, what am I going to do with it? Probably nothing. The Amateur Reasoner was like Prolog without the variables, mainly because I couldn't figure out then how to implement the variables and attach values during a recursive process. I also note that in the 'flagship program of the Occupational Therapist', I did something similar, using rules stored in an sql database.

No comments: