Eliza and the Beginning of Artificial Intelligence

Eliza and the Beginning of Artificial Intelligence

Now that we've covered arrays and collections, let's do something really fun. Let's write a rudimentary artificial intelligence program that will solidify your understanding of arrays and collections and build on your knowledge of how to use the .NET Framework classes.

Back in the dim dark ages of computer science, 1963 to be exact, Dr. Joseph Weizenbaum was working away at MIT, struggling to understand artificial intelligence. He was working on a crude attempt at making a computer understand the English language. Weizenbaum named his program Eliza after George Bernard Shaw's character Eliza Doolittle, from his play Pygmalion. The program is simple but can provide what appears to be stunning and profound insight to the uninitiated.

In the program, the patient types a response to a question and presses the Enter key. The program parses each word of the patient's response using the String class's Split method and places each word in an array element. Our program then quickly scans each word to see whether a match is found in a two-dimensional array in our program. Using arrays and the .NET String, StringBuilder, StringCollection, and Random classes, our program displays a response from the doctor that is eerily humanlike.

We know, of course, that the program has no idea what is being typed but is simply manipulating strings. However, users of the program find it strangely compelling. In fact, during the course of developing the original Eliza, some of the "patients" became so attached to the program that Dr. Weizenbaum was appalled. His secretary, for example, saw him developing the program over six months and knew what he was doing. She knew that there was no real intelligence, just word manipulation. Yet, when he asked her to try it out, after a few minutes she started telling Eliza her innermost secrets. She then asked him to leave the room so she could be alone with the program. Dr. Weizenbaum was so upset at how easy it was for humans to turn control over to a machine that he wrote the book Computer Power and Human Reason, in which he roundly renounced artificial intelligence.

Be that as it may, Eliza has surfaced again at many college campuses. I wrote a version many years ago in Basic on a Commodore 64 computer and discovered that people had a reaction similar to that Dr. Weizenbaum experienced so many years before. So who knows, after you write Visual Basic .NET Eliza, your friends might actually become attached to your computer.

Eliza in Action

Eliza mimics a nondeterminist Rogerian psychologist. Sometimes the program repeats what the patient says in an open-ended way to encourage more comments. Being from the nondeterminist school, the program does not judge but simply uses the patient's own words to encourage more responses. Eliza also asks unrelated questions that appear to the user to be profound. Figure 6-21 shows the Eliza program.

Figure 6-21

Eliza in action.

The program takes what the user says and turns it into a question. It also does some sleight of hand to change a phrase like "I'm tired" into "Why are you tired?", shown in Figure 6-22. This sort of response can give the user the feeling that the program actually understands the context of the question. Of course, we are simply using classes from the .NET Framework for a smoke and mirrors effect.

Figure 6-22

Eliza takes the user's text and turns it into a question.

This sample dialog shows that the responses vary just enough so that the user can't be quite sure whether the software is intelligent or not. The program then asks a question from out of left field that the user thinks might have something to do with learning Visual Basic .NET.

Doctor: "Why are you trying to learn Visual Basic .NET?"

Patient: "Why are you so interested?"

Doctor: "Why am I so interested?"

Patient: "Yes, are you a programmer?"

Doctor: "We are discussing you, not me."

Patient: "I'm learning Visual Basic .NET to do great things."

Doctor: "Why are you learning Visual Basic .NET to do great things?"

Patient: "Because it's the next great thing."

Doctor: "Do your friends really like you?"

Patient: "It's hard to tell."

Doctor: "Did you fight with your family?"

The doctor will continue like this as long as the patient keeps typing. The program also has some randomness built in so that at certain times the doctor asks questions about what the patient said earlier in the session. This touch gives the appearance that the program has memory, which it does, thanks to the .NET StringCollection class. As you can see, this is a program that you'll have fun building and is an excellent way to understand more about the framework classes and collections.

note

One researcher built a paranoid version of Eliza, which he named Perry. Perry would provide responses that were as paranoid as those of the user. His program would ask questions about horse racing and gambling. Once you have built Eliza, you will find many ways to modify or upgrade the program. You might even stream out the patient-doctor dialogue to a file for later review. And you can certainly expand Eliza's vocabulary to make the program seem brilliant. All in all, you can have some fun with your friends.

Coding Eliza

Start a new Visual Basic .NET Windows program named Eliza. Using the default form, add three labels. The first label is for "The Computer Psychiatrist." The second is to display the doctor's responses. The third is used for the instructions to the user. Add a text box under the instructions. The text box will be used to accept the user's input. Your form should look like the one in Figure 6-23. Once you've set up your form, set the values for the properties listed in Table 6-4.

Figure 6-23

The form for the Eliza program.

You might notice that with the newly redesigned Visual Basic .NET controls, both the text box and the label use the Text property to display text. Because .NET controls don't have default properties, you can't use statements such as lblDoctor = "How are you?". Instead you must use the fully qualified lblDoctor.Text = "How are you?".

Table 6-4  Properties for the Eliza Program

Object

Property

Value

Form1

BorderStyle

FixedDialog

Icon

Microsoft Visual Studio .NET\Common7\Graphics\Icons\ Comm\Handshak

MaximizeBox

False

MinimizeBox

False

Text

ELIZA, the computer doctor

Label1

Name

lblCaption

Font

Comic Sans MS

Font Style: Bold

Size: 11

Text

The Computer Psychiatrist

TextAlign

MiddleCenter

Label2

Name

lblDoctor

Backcolor

Lime (Pick from Web Tab)

BorderStyle

Fixed3D

Text

" "

Label3

Name

lblInstructions

Text

Type your response and press Enter

TextBox1

Name

txtQuestion

BorderStyle

Fixed3D

Text

" "

We won't have much code in the form. We will place most of the code in a module that will be the brains of our system. From the main IDE menu, select Project | Add Module. In the Add New Item dialog box, shown in Figure 6-24, select a new module and give it the name Dialog.vb. Click Open to add the new empty code module to your project.

Figure 6-24

Add a new code module to your project.

Topology of Our Dialog.vb Code Module

Our new module will have five main capabilities. The first is a public function, named getElizaResponse, that acts as an internal dispatcher. When the user hits the Enter key, the contents of the text box are passed to this function. The first operation the function performs is to stash away the current patient comment in a StringCollection object for later use. It also increments a private counter to determine how many times the patient responded to the doctor.

Next getElizaResponse tries each of five private routines that attempt to return some profound comment to the user. First it calls isDiscussingDoctor, which returns a Boolean. If the patient is discussing the doctor, which we determine by finding the word you in the patient's response, an annoyed response is sent back to the patient.

If that routine is not successful in matching a key word, control is passed to the function getQuickResponse. Here we determine whether other specific words are in the patient's response. For example, if the code finds the word mother in the patient's response, it will return "Tell me more about your family…" This response gives the impression that Eliza understands family dynamics. Nothing but smoke and mirrors, but effective.

If getQuickResponse fails, program control is dispatched to the private routine tryToTranslate, which attempts to find key words from the patient's response in an array and swaps them with alternate words. For example, if the word "I" is in a response, this routine changes it to you. If the user types "I like Visual Basic .NET," this routine finds the word I and modifies the program's reply to say "You like Visual Basic .NET?" This routine provides the nondeterministic capability of Eliza.

Another trick up our sleeve is a special piece of code that stores all of the user's responses in another StringCollection. Under certain conditions, this code generates a random number between 0 and the number of patient responses so far. You will learn about the framework's Random class from this piece of code. When the conditions are met, Eliza says, "Please tell me more about < one of the user's previous responses>," giving the appearance of memory and that the program is tying together thoughts from the session.

If that routine comes up empty, we simply display a stock phrase from an array by calling getStockPhrase. We keep track of the last stock phrase displayed so we can always provide a new one. All of this management is performed by getElizaResponse. The function takes in the patient's response as a parameter and manages the various internal routines to eventually provide a response to the user.

The beauty of this approach is that our module is self-contained. The host (in this case the form) only has to ask for a request. The Dialog.vb module initializes variables and arrays and determines the response to send back. Figure 6-25 shows the sequence in which the appropriate response is generated.

Figure 6-25

How Eliza determines a response.

Code Module vs. Class Module

You might be wondering why I elected to use a code module instead of a class for this project. I could have very easily made this code a class; however, three main architectural reasons favor the module approach. First, the primary reason to build classes is reusability. We won't reuse this code over and over, so we simply placed everything in a module. As you know from working with Visual Basic 6, modules are simply warehouses for code. Second, we won't be instantiating multiple instances of the dialog code. The functionality will simply be imported into our host. Third, we are not setting properties or state in any way from the outside world. The host asks for a response and the module supplies it. As you build more classes and modules, you will find you are making design decisions like this often.

In almost all cases, it's good practice to separate the code from the user interface. In general, with this separation you can modify either the form or the module and the other does not have to be changed. Independence (or modularity) is the key to keeping modules from becoming too dependent on each other. The Eliza design is an example of that concept.

Writing the Dialog.vb Code Module

The next step in building the Eliza program is to delete the template code in the Dialog.vb code module and add the code that follows. You'll learn quite a bit about the .NET Framework in this module. Notice that everything is declared private except the getElizaResponse function, which is the traffic cop for our module. It dispatches routines and returns the correct response. This approach permits us to keep any knowledge of how Dialog.vb works from the outside world. All that is publicly exposed is getElizaResponse.

Imports System.Collections Imports System.Text ' For StringBuilder Module Dialog Private sPatientResponse As String = "" 'Holds patient comment Private sDoctorResponse As String = "" 'Holds Dr response Private bInitEliza As Boolean = False 'Is array initialized Private cBlank() As Char = {CChar(" ")} 'For splitting ' sentences Private aTranslate(,) As String 'Array for swapping Private iTalkingAboutMe As Integer = 0 'How many times ' 'you' used Private iPatientResponseCount As Integer = 0 'Number of patient ' comments Private cPatientsResponses As New _ Collections.Specialized.StringCollection() Private sStockResponse() As String = _ {"Do your friends really like you?", _ "Did you fight with your family?", _ "Did you have a happy childhood?", _ "Did you hate your father?", _ "Are you afraid of your friends?", _ "Why are you so angry?", _ "Tell me about your involvement with horse racing.", _ "Does the name Ruby Begonia mean anything to you?", _ "Why do you have such dark secrets?", _ "Why are you obsessed with your mortality?", _ "Tell me about your criminal background."} Private Sub initaTranslate() '-- This is called to initialize the array to illustrate '-- ReDim and another way to init an array. ReDim aTranslate(7, 1) aTranslate(0, 0) = "i" aTranslate(0, 1) = "you" aTranslate(1, 0) = "you" aTranslate(1, 1) = "I" aTranslate(2, 0) = "your" aTranslate(2, 1) = "my" aTranslate(3, 0) = "my" aTranslate(3, 1) = "your" aTranslate(4, 0) = "am" aTranslate(4, 1) = "are" aTranslate(5, 0) = "you" aTranslate(5, 1) = "i" aTranslate(6, 0) = "are" aTranslate(6, 1) = "am" aTranslate(7, 0) = "me" aTranslate(7, 1) = "you" End Sub Public Function getElizaResponse( _ ByVal patientResponse As String) As String '-- This function acts as the dispatcher. It attempts to '-- get a response first from tryToTranslate. If that fails, '-- it then tries getQuickResponse. Finally, if all else '-- fails a call to getStockPhrase is made. sDoctorResponse = "" '— No sense going any farther If patientResponse.Length = 0 Then Return "Please enter a question." End If '-- Now the response is visible module wide. Make it lower '-- case for easier string matching in the routines. sPatientResponse = patientResponse.ToLower '-- Add the patient's comment for later display iPatientResponseCount += 1 cPatientsResponses.Add(sPatientResponse) '-- Has the aTranslate array been initialized? -- If bInitEliza = False Then initaTranslate() bInitEliza = True End If '------------------------------------------------------ '-- Now we simply call the various functions that will '-- build the Dr's response if appropriate. If any of the '-- functions return True, then return the phrase. '------------------------------------------------------- '-- See if the patient is talking about the good '-- doctor again -- If isDiscussingDoctor() = True Then Return sDoctorResponse End If '-- Can we get a quick and dirty response? -- If getQuickResponse() = True Then Return sDoctorResponse End If '-- See if there are any words to substitute -- If tryToTranslate() = True Then Return sDoctorResponse End If '-- Can we return an earlier patient phrase? -- If getRandomPhrase() = True Then Return sDoctorResponse End If '-- When all else fails, get a stock phrase sDoctorResponse = getStockPhrase() Return sDoctorResponse End Function Private Function isDiscussingDoctor() As Boolean '-- Here we can see if the word 'you' is present. If so, '— increment iTalkingAboutMe. Every other time the patient '-- uses 'you', complain. If (sPatientResponse.IndexOf("you") > -1) Then iTalkingAboutMe += 1 '-- Display irritation every other time the Dr is mentioned. If (iTalkingAboutMe Mod 2 = 0) Then If (iTalkingAboutMe < 3) Then sDoctorResponse = "We are discussing you, not me" Else sDoctorResponse = "You have talked about me " & _ iTalkingAboutMe.ToString & _ " times. Shall we focus on you?" End If Return True End If End If Return False End Function Private Function getQuickResponse() As Boolean '-- Here we see if we can match a word in the patient's '-- comment. If we can, let's return a response from the Dr. Dim iIndex As Integer = 0 If (sPatientResponse.IndexOf("yes") > -1) Then sDoctorResponse = "Ah...that is positive. Tell me more." Return True ElseIf (sPatientResponse.IndexOf("hate") > -1) Then sDoctorResponse = "Why are you so angry?" Return True ElseIf (sPatientResponse.IndexOf("mother") > -1) Then sDoctorResponse = "Tell me more about your family..." Return True ElseIf (sPatientResponse.IndexOf("father") > -1) Then sDoctorResponse = "Why were you angry at males in " & _ "your family?" Return True ElseIf (sPatientResponse.IndexOf("sister") > -1) Then sDoctorResponse = "Why are you jealous of your sister?" Return True ElseIf (sPatientResponse.IndexOf("brother") > -1) Then sDoctorResponse = "Why was your brother liked more " & _ "than you?" Return True ElseIf (sPatientResponse.IndexOf("you are") > -1) Then iIndex = sPatientResponse.IndexOf("you are") sDoctorResponse = "I am " & _ sPatientResponse.Substring(iIndex + 8) & "?" Return True ElseIf (sPatientResponse.IndexOf("i am") > -1) Then iIndex = sPatientResponse.IndexOf("i am") sDoctorResponse = "Why are you " & _ sPatientResponse.Substring(iIndex + 5) & "?" Return True ElseIf (sPatientResponse.IndexOf("i'm") > -1) Then iIndex = sPatientResponse.IndexOf("i'm") sDoctorResponse = "Why are you " & _ sPatientResponse.Substring(iIndex + 4) & "?" Return True ElseIf (sPatientResponse.IndexOf("we ") > -1) Then sDoctorResponse = "Try not to discuss us – " & _ "tell me about you." Return True ElseIf (sPatientResponse.IndexOf("no ") > -1) Then sDoctorResponse = "Why are you so negative?" Return True ElseIf (sPatientResponse.IndexOf("weather") > -1) Then sDoctorResponse = "Did you want to be a " & _ "meteorologist as a child?" Return True Else Return False 'No quick response End If End Function Private Function tryToTranslate() As Boolean Dim aSentenceWord() As String = _ sPatientResponse.Split(cBlank) Dim iWordsLower As Integer = _ aSentenceWord.GetLowerBound(0) Dim iWordsUpper As Integer = _ aSentenceWord.GetUpperBound(0) Dim iaTranslateLower As Integer = _ aTranslate.GetLowerBound(0) Dim iaTranslateUpper As Integer = _ aTranslate.GetUpperBound(0) Dim sbDrResponse As New StringBuilder() Dim iWordLoop As Integer = 0 Dim iTranslateLoop As Integer = 0 Dim sCurrentWord As String = "" Dim bCanTranslate As Boolean = False Dim bAddQuestionMark As Boolean = False '-- Let's see if we can parse the patient's comment and '-- substitute words from the translate array. For iWordLoop = iWordsLower To iWordsUpper sCurrentWord = aSentenceWord(iWordLoop) bCanTranslate = False For iTranslateLoop = iaTranslateLower To iaTranslateUpper '-- If the current word is in the first aTranslate array '-- element, then substitute with the second element If aTranslate(iTranslateLoop, _ 0).Equals(sCurrentWord) Then If (iWordLoop = 0) Then sbDrResponse.Append(aTranslate(iTranslateLoop, _ 1).TrimStart(cBlank)) Else sbDrResponse.Append(" " & _ aTranslate(iTranslateLoop, _ 1).TrimStart(cBlank)) End If bCanTranslate = True bAddQuestionMark = True Exit For End If Next '-- If we couldn't swap, add the current word If (bCanTranslate = False) Then If (iWordLoop = 0) Then sbDrResponse.Append(sCurrentWord) Else sbDrResponse.Append(" " & sCurrentWord) End If End If Next '-- If we were successful, append a ? to the '-- end and trim any leading blank spaces. '— Recall our brief discussion of short-circuiting. '-- If bAddQuestionMark is False, the length of '-- sbDrResponse is not checked – the code jumps '-- to the Else statement. If (bAddQuestionMark = True) And (sbDrResponse.Length > 5) Then Dim sFinalResponse As String Dim cFirstLetter As Char = CChar(sbDrResponse.Chars(0)) '-- Extract the first letter and capitalize cFirstLetter = cFirstLetter.ToUpper(cFirstLetter) '-- Remove the first letter and replace it with cap sbDrResponse.Remove(0, 1) sbDrResponse.Insert(0, cFirstLetter) '-- Add a ? and return the string sbDrResponse.Append("?") sDoctorResponse = sbDrResponse.ToString Return True Else '-- No luck, return an empty string Return False End If End Function Private Function getRandomPhrase() As Boolean If (iPatientResponseCount Mod 6 = 0) Then Dim iLimit As Integer = cPatientsResponses.Count Dim iPickResponse As Integer = 0 Dim iRandom As New Random() Dim sPreviousComment As New StringBuilder() ' Be sure to ' use New Dim sComment As String '-- Which random response to select from the '-- cPatientsResponses collection? iPickResponse = iRandom.Next(0, iLimit - 1) If (iPickResponse <= cPatientsResponses.Count) Then Dim cFirstLetter As Char '-- Retrieve the comment to display sComment = cPatientsResponses(iPickResponse) '-- Take the first letter of the retrieved comment cFirstLetter = CChar(sComment.Chars(0)) '-- Capitalize it cFirstLetter = cFirstLetter.ToUpper(cFirstLetter) '-- Remove the first letter and replace it with cap sComment = sComment.Remove(0, 1) sComment = sComment.Insert(0, cFirstLetter) '-- Now construct the string for the Dr's response -- sPreviousComment.Append("Please tell me more about: """) sPreviousComment.Append(sComment & """") sDoctorResponse = sPreviousComment.ToString Return True End If End If Return False End Function Private Function getStockPhrase() As String Static iCurrentStock As Integer = 0 If iCurrentStock > sStockResponse.GetUpperBound(0) Then iCurrentStock = sStockResponse.GetLowerBound(0) + 1 Return sStockResponse(iCurrentStock - 1) Else iCurrentStock += 1 Return sStockResponse(iCurrentStock - 1) End If End Function End Module

Examining Our Code

The Eliza program includes a lot of code, so let's look at most of it in detail and solidify our understanding of the operations it performs. Of course we import System as usual, but in this case we also import System.Text. Importing System.Text gives us access to the StringBuilder class. Recall from Chapter 5, "Examining the .NET Class Framework Using Files and Strings," that if you are going to do a lot of string manipulation, the StringBuilder class is faster than String because it allocates a buffer in memory. StringBuilder modifies the same memory location for any modifications to the string. When the String class is used, we end up destroying the current string and creating a new one for each modification, which is expensive in CPU currency.

Module-Level Variables

We declare several variables at the top of the module so that they have full module scope. For example, both the patient's response and the doctor's response need to be available in all modules. It's cheaper to assign them to module-level variables instead of passing them as parameters to each function that needs them.

There are a couple of items that we need to review. The first is the cBlank array of the Char data type. In Chapter 5, I described the String class's Split method, which can take a string and break up its contents into an array. The Split method requires a character to use as the separator, and we are using a blank space as that separator. When this separator character is passed to the Split method, every word separated by a space will be placed in its own array element. We can then easily examine each word in the patient's response. Finally in this section of the code we dimension a new StringCollection object that will be used to hold each of the patient's responses during each session.

Private sPatientResponse As String = "" 'Holds patient comment Private sDoctorResponse As String = "" 'Holds Dr response Private bInitEliza As Boolean = False 'Is array initialized Private cBlank() As Char = {CChar(" ")} 'For splitting ' sentences Private aTranslate(,) As String 'Array for swapping Private iTalkingAboutMe As Integer = 0 'How many times ' 'you' used Private iPatientResponseCount As Integer = 0 'Number of patient ' comments Private cPatientsResponses As New _ Collections.Specialized.StringCollection()

The declaration for the two-dimensional array aTranslate might look a bit strange to Visual Basic 6 programmers; the single comma inside the parentheses looks like a mistake. If you are familiar with the Visual Basic 6 Collection object, the StringCollection concept is similar.

In this sample program, I've used several approaches to dimensioning arrays to illustrate their flexibility. In the following code, I initialize a string array with several strings, separated by commas. When I do this, the array is automatically sized to fit the number of entries. New to Visual Basic .NET are the "{" and "}" characters used to initialize an array, as shown in sStockResponse. At any time, you could go back to the source code and add another line or two and the array will compensate and resize itself when the program is run.

Private sStockResponse() As String = _ {"Do your friends really like you?", _ "Did you fight with your family?", _ "Did you have a happy childhood?", _ "Did you hate your father?", _ "Are you afraid of your friends?", _ "Why are you so angry?", _ "Tell me about your involvement with horse racing.", _ "Does the name Ruby Begonia mean anything to you?", _ "Why do you have such dark secrets?", _ "Why are you obsessed with your mortality?", _ "Tell me your criminal background."}

Arrays vs. Collections

We store the patient's responses in a StringCollection object. Why didn't we use an array?

Private cPatientsResponses As New _ Collections.Specialized.StringCollection()

A collection is not a replacement for an array but a complement. As you saw earlier in the chapter, arrays have a fixed size and structure. If you dimension an array of integers and place the value 42 in element 5, you know that value will be there when you need it. By dereferencing element 5 of our array (iMeaning- OfLife = aCosmic(5)), we can be sure that our variable will contain 42. However, if you don't know how big to make an array at design time, you have to guess at a size and constantly check to see whether adding another object will overstep the upper bound. Code then needs to be added to redimension the array to accept more items.

Remember that a collection, on the other hand, grows automatically when an object is added. Likewise, when an element is deleted, the collection frees up the memory from the deleted entry automatically. For example, Visual Basic manages a collection of forms you have in a project. You don't have to worry about tracking them; it's done for you. Likewise, each form manages its own collection of the controls it contains. It's important to understand collections because they are used everywhere in the .NET Framework. When you want to grab all of the directories on a machine, a collection is used to hold them. When you need to get all the files in a folder, a collection of folders is interrogated. In the Eliza program, once you understand how to use the StringCollection in the System.Text namespace, you will be able to use any collection in the .NET Framework.

The Entry Point for Eliza

The public function getElizaResponse is used to call our module. The patient's response is passed in as a parameter using ByVal. Recall that the default way of passing in a parameter to a procedure in Visual Basic .NET is by using ByRef, whereas in classic Visual Basic the default was ByVal. When a parameter is passed with ByVal, a copy of the object is passed in. You can change it in any way, but because you are only modifying a copy, the original object is unscathed. If we pass in a parameter with ByRef, an actual reference (in other words, a pointer to the memory location of the object) is passed in. If you make changes to a ByRef variable, the original is also modified because the reference and the original are one and the same.

We initialize sDoctorResponse for each call to getElizaResponse. It will still contain the doctor's previous response. We then check to see whether the patient actually entered a question. If not, we pass back instructions and exit the dialog. If there is a response, we store the patient's response in the module-level variable sPatientResponse. Notice that we cast it to lowercase because we will be doing a boatload of string comparisons for key words. If we know everything is in lowercase, our code is easer to manage because we don't have to check for caps at the beginning of a sentence, for example. Next we increment the number of responses the patient has made so far and then add the current patient's response to our StringCollection object.

Public Function getElizaResponse( _ ByVal patientResponse As String) As String '-- This function acts as the dispatcher. It attempts to '-- get a response first from tryToTranslate. If that fails, '-- it then tries getQuickResponse. Finally, if all else '-- fails a call to getStockPhrase is made. sDoctorResponse = "" '— No sense going any farther If patientResponse.Length = 0 Then Return "Please enter a question." End If '-- Now the response is visible module wide. Make it lower '-- case for easier string matching in the routines. sPatientResponse = patientResponse.ToLower '-- Add the patient's comment for later display iPatientResponseCount += 1 cPatientsResponses.Add(sPatientResponse)

In a real program, you probably would not initialize an array and then check each time a function is called to see whether it was already initialized. However, I wanted to show you another way to initialize an array. We check whether the Boolean is True. If not, initialize the array and set bInitEliza to True so that this block of code will not be executed again. A kludge, but good for illustrating a concept.

 '-- Has the aTranslate array been initialized? -- If bInitEliza = False Then initaTranslate() bInitEliza = True End If

Now that the housekeeping has been completed, we simply call each of the five private functions. Each one in turn examines the module-level variable sPatientResponse and determines whether it can do something intelligent with it. If it can, that routine sets the module level variable sDoctorResponse and returns True. The first function to return True is the response that is returned to the calling form. If the first four routines come up empty, we use getStockResponse as our fallback to return a predefined response from an array.

One difference between Visual Basic .NET and classic Visual Basic is the return statement. In classic Visual Basic, you had to assign the return value to the name of the function. Now you can simply use the keyword Return. Of course, the return value must be consistent with the function's signature. In this function's signature, we define the return data type as a string.

Public Function getElizaResponse( _ ByVal patientResponse As String) As String

When Return is encountered, program control is immediately returned to the function caller.

 '------------------------------------------------------ '-- Now we simply call the various functions that will '-- build the Dr's response. If any of the '-- functions return True, then return the phrase. '------------------------------------------------------- '-- See if the patient is talking about the good doctor again -- If isDiscussingDoctor() = True Then Return sDoctorResponse End If '-- Can we get a quick and dirty response? -- If getQuickResponse() = True Then Return sDoctorResponse End If '-- See if there are any words to substitute -- If tryToTranslate() = True Then Return sDoctorResponse End If '-- Can we return an earlier patient phrase? -- If getRandomPhrase() = True Then Return sDoctorResponse End If '-- When all else fails, get a stock phrase sDoctorResponse = getStockPhrase() Return sDoctorResponse End Function

Remember that one of the first operations that getElizaResponse performs is to check whether the array initaTranslate is initialized using the Boolean bInitEliza as a flag. The first time the program is used, the variable is initialized to False, so initaTranslate is called. When we declared the array with the module-level variables, we told Visual Basic .NET it would be two-dimensional and hold strings.

Private aTranslate(,) As String

Notice that we now use the ReDim statement to modify the array aTranslate so that it holds eight rows and two columns. In production code, always try to stay away from using ReDim. As I mentioned earlier, the ReDim statement instantiates a new array each time it's used. So, if you are checking to see how many items are in the array and you need to add one more, you use the ReDim statement on the array. The original array is deleted and a copy is made with the new bounds. This process is very expensive. And when you use ReDim, if by accident you specify a size that's too small, a System.InvalidCastException will be thrown when your program attempts to access an out-of-bounds element.

Using the ReDim statement with aTranslate gives us the first dimension of 8 (0–7) and the second dimension of 2 (0–1). In the first dimension we add a word that the patient might place in a response. If a word is found, the word in the second dimension is returned. If the word i is found, we will return you. Now you can see why we made the patient response all lowercase. It's easy to compare words. This array is used extensively in the function tryToTranslate. Each word in the patient's response is compared to each of the 0th elements for each row. If a match is found, that row's first element is returned and placed in the response.

Private Sub initaTranslate() '-- This is called to initialize the array to illustrate '-- ReDim and another way to init an array. ReDim aTranslate(7, 1) aTranslate(0, 0) = "i" aTranslate(0, 1) = "you" aTranslate(1, 0) = "you" aTranslate(1, 1) = "I" aTranslate(2, 0) = "your" aTranslate(2, 1) = "my" aTranslate(3, 0) = "my" aTranslate(3, 1) = "your" aTranslate(4, 0) = "am" aTranslate(4, 1) = "are" aTranslate(5, 0) = "you" aTranslate(5, 1) = "i" aTranslate(6, 0) = "are" aTranslate(6, 1) = "am" aTranslate(7, 0) = "me" aTranslate(7, 1) = "you" End Sub

Conceptually, the array looks like the following. A two-dimensional array is much like a spreadsheet. The elements 0 through 7 are the rows, and the elements 0 through 1 are the columns. You simply address the row and column (in that order) to retrieve the value you are looking for.

(0,0) "i"

(0,1) "you"

(1,0) "you"

(1,1) "I"

(2,0) "your"

(2,1) "my"

(3,0) "my"

(3,1) "your"

(4,0) "am"

(4,1) "are"

(5,0) "you"

(5,1) "i"

(6,0) "are"

(6,1) "am"

(7,0) "me"

(7,1) "you"

Is the Patient Discussing the Good Doctor?

We want to be sure our psychiatric session stays on track. One of the first things a new patient does is try to discuss the doctor. We want to be sure the patient knows that this will not help him or her. The conversation should be centered on the patient. This function returns True or False depending on whether the patient discussed the doctor, and takes further action if other conditions are met.

We simply check whether the substring "you" was found in the patient's response, indicating a reference to the doctor. If this is true, we increment the module-level variable iTalkingAboutMe. We use this variable to determine whether we should provide an annoyed response to the user.

Private Function isDiscussingDoctor() As Boolean '— Here we can see if the word you is present. If so, '— increment iTalkingAboutMe. Every other time the patient '-- uses 'you', complain. If (sPatientResponse.IndexOf("you") > -1) Then iTalkingAboutMe += 1

We want to provide a response to the user only every other time the word you is used. The easiest way to accomplish this is with the Mod operator. The modulo operator computes the remainder of the division of two operands. If you have never used the Mod operator, you will be amazed at how simple it is. (Modulus is a fancy word for remainder.) So, the Mod operator divides two numbers and returns only the remainder. If either number is a floating-point number, the result is a floating-point number representing the remainder.

For example, in the following expression, iResult (result, i.e., remainder or modulus) equals 2.

iResult = 8 Mod 3

Using Mod, we can easily handle every other occurrence of the patient using the word you. We take how many times the patient has mentioned the doctor and divide it by 2. If the remainder is 1, it's either the first, third, fifth (and so on) time the word was used. If the remainder is 0, it's every other time. If we want to respond every fifth time, simply change Mod 2 to Mod 5 and check whether the result is equal to 0. By making the doctor respond every other time the word you is used instead of every time, we make the doctor appear unpredictable.

'-- Display irritation every other time the Dr is mentioned. If (iTalkingAboutMe Mod 2 = 0) Then

The rest of this routine is straightforward. If a response is the first time the patient has referred to the doctor, the doctor simply attempts to redirect the conversation. However, if this happens more than once, the doctor points out exactly how many times he has been mentioned.

 If (iTalkingAboutMe < 3) Then sDoctorResponse = "We are discussing you, not me" Else sDoctorResponse = "You have talked about me " & _ iTalkingAboutMe.ToString & _ " times. Shall we focus on you?" End If Return True End If End If Return False End Function

Can Eliza Return a Quick Response?

If the patient did not discuss the doctor, the next function called is getQuickResponse. This function looks for key words within the patient's response. In some cases, if a word is found, a stock response is returned. For example, if the word mother is used, the doctor asks to learn more about the family. Sounds profound, eh? Because the doctor's response is visible at the module level, it's set here and can be returned by getElizaResponse. If a key word is found, the function returns True and control is returned to the calling function.

Private Function getQuickResponse() As Boolean '-- Here we see if we can match a word in the patient's '-- comment. If we can, let's return a response from the Dr. Dim iIndex As Integer = 0 If (sPatientResponse.IndexOf("yes") > -1) Then sDoctorResponse = "Ah...that is positive. Tell me more." Return True ElseIf (sPatientResponse.IndexOf("hate") > -1) Then sDoctorResponse = "Why are you so angry?" Return True ElseIf (sPatientResponse.IndexOf("mother") > -1) Then sDoctorResponse = "Tell me more about your family..." Return True ElseIf (sPatientResponse.IndexOf("father") > -1) Then sDoctorResponse = "Why were you angry at males in " & _ "your family?" Return True ElseIf (sPatientResponse.IndexOf("sister") > -1) Then sDoctorResponse = "Why are you jealous of your sister?" Return True ElseIf (sPatientResponse.IndexOf("brother") > -1) Then sDoctorResponse = "Why was your brother liked more " & _ "than you?" Return True

A few of the key words can be used to turn around a patient's response to provide the nondeterministic aspect of the doctor's replies. For example, if the patient says, "You are a dolt," there is a 50/50 chance that the quick response function will return a message because of the word you. However, if the Mod operator returns a 1, the function returns False and this function will find the substring "you are". We simply make the doctor's response be "I am," and add the rest of the patient's comments. In this example, the doctor would return, "I am a dolt?"

When we are translating, it's important to get the exact position of the words we are exchanging. For example, if the user enters "Because, I am depressed," we want to grab the start of the string and then add to it the number of spaces in the string and return the remainder. If we didn't do this, our response would pick up fragments of words.

ElseIf (sPatientResponse.IndexOf("you are") > -1) Then iIndex = sPatientResponse.IndexOf("you are") sDoctorResponse = "I am " & _ sPatientResponse.Substring(iIndex + 8) & "?" Return True ElseIf (sPatientResponse.IndexOf("i am") > -1) Then iIndex = sPatientResponse.IndexOf("i am") sDoctorResponse = "Why are you " & _ sPatientResponse.Substring(iIndex + 5) & "?" Return True ElseIf (sPatientResponse.IndexOf("i'm") > -1) Then iIndex = sPatientResponse.IndexOf("i'm") sDoctorResponse = "Why are you " & _ sPatientResponse.Substring(iIndex + 4) & "?" Return True

You can add as many responses as you want to extend Eliza and make the program seem truly intelligent. Be careful when scanning for individual words because they could be contained in larger words. The program looks for the word we by adding a space after it. Without this precaution, if the patient entered a substring with the word welcome, it would be picked up and an inappropriate response would be returned. And because everyone wants to discuss the weather, we added a response for that as well. If we can't provide a quick response, the function returns False.

ElseIf (sPatientResponse.IndexOf("we ") > -1) Then sDoctorResponse = "Try not to discuss us – " & _ "tell me about you." Return True ElseIf (sPatientResponse.IndexOf("no ") > -1) Then sDoctorResponse = "Why are you so negative?" Return True ElseIf (sPatientResponse.IndexOf("weather") > -1) Then sDoctorResponse = "Did you want to be a " & _ "meteorologist as a child?" Return True Else Return False 'No quick response End If End Function

Can Eliza Translate the Patient's Response to Make It a Question?

This routine is the most complicated in our module. Essentially we take each word in the patient's response and see whether it is contained in the aTranslate array. If it is, a substitution is made.

We pass in the cBlank character to the Split function of the sPatientResponse string. This breaks up the string into each word that is separated by a blank space and returns an array of the words. We store the array of words returned in the sSentenceWord array. We then cache the lower and upper bounds of the aSentenceWord array in two integer variables for quick looping. Another variable of interest is sbDrResponse, which has the data type StringBuilder. Because we will do quite a bit of string manipulation to the response in this routine, a StringBuilder is made to order.

Private Function tryToTranslate() As Boolean Dim aSentenceWord() As String = _ sPatientResponse.Split(cBlank) Dim iWordsLower As Integer = _ aSentenceWord.GetLowerBound(0) Dim iWordsUpper As Integer = _ aSentenceWord.GetUpperBound(0) Dim iaTranslateLower As Integer = _ aTranslate.GetLowerBound(0) Dim iaTranslateUpper As Integer = _ aTranslate.GetUpperBound(0) Dim sbDrResponse As New StringBuilder() Dim iWordLoop As Integer = 0 Dim iTranslateLoop As Integer = 0 Dim sCurrentWord As String = "" Dim bCanTranslate As Boolean = False Dim bAddQuestionMark As Boolean = False

We now loop through each word in the aSentenceWord array and assign each in turn to sCurrentWord. We also assume that we can't translate the word, so the Boolean bCanTranslate is initialized to False for each time through the loop. This flag is reset when we are successful.

'-- Let's see if we can parse the patient's comment and '-- substitute words from the translate array. For iWordLoop = iWordsLower To iWordsUpper sCurrentWord = aSentenceWord(iWordLoop) bCanTranslate = False

For each word that is stored in sCurrentWord, we compare it to the contents of the first element of the aTranslate array to see whether we can substitute for it. Notice that we use the Equals method of the aTranslate array.

For iTranslateLoop = iaTranslateLower To iaTranslateUpper '-- If the current word is in the first aTranslate array '-- element, then substitute with the second element If aTranslate(iTranslateLoop, 0).Equals(sCurrentWord) Then

If the current word matches one of the words in the 0th position of the aTranslate array, we append it to the doctor's response, which will be stored in the StringBuilder variable sbDrResponse. Because sbDrResponse is a locally scoped variable, it's created when this function is entered and goes out of scope when the function is exited. Therefore, the first time through the loop, the variable sbDrResponse is empty.

If the word that's matched happens to be the first word in the sentence, the returned word is appended to the front of the StringBuilder. We take the contents of the aTranslate array in row iTranslateLoop, column 1, and trim off any blank spaces.

If the matched word is from the body of the patient's response, for example "Because I am depressed," we append it to what is already there. Let's say the user types "Because I am depressed." The first word would not be found, but the second word would be. Because iWordLoop would be 1, we append "you" to sbDrResponse. Because we found at least one word, we set the two Booleans to True.

 If (iWordLoop = 0) Then sbDrResponse.Append(aTranslate(iTranslateLoop, _ 1).TrimStart(cBlank)) Else sbDrResponse.Append(" " & _ aTranslate(iTranslateLoop, _ 1).TrimStart(cBlank)) End If bCanTranslate = True bAddQuestionMark = True Exit For End If Next

If the current word was not in the aTranslate array, it is appended to sbResponse; otherwise we add " " before the word.

 '-- If we couldn't swap, add the current word If (bCanTranslate = False) Then If (iWordLoop = 0) Then sbDrResponse.Append(sCurrentWord) Else sbDrResponse.Append(" " & sCurrentWord) End If End If Next

If we take our example "Because I am depressed," the first time through the loop the word because was not found so it is appended to sbDrResponse. The next time through the loop the word examined is I, which is found. Because I is not the first word, you is appended to sbDrResponse. Our StringBuilder variable now equals "because you." We make everything lowercase, so any words we use from the patient's response that have capital letters will need to be fixed. We will do this soon.

The next words in the patient's response, are depressed, are not found. That means they are appended to the StringBuilder variable as is. As you can see, the result becomes "because you are depressed."

The doctor's response is now built. If we find at least one word in the aTranslate array and the length of the doctor's response is greater than 5, we will add the finishing touches. First, we take the first character of the final response, which in our example will be b, and assign it to the variable cFirstLetter. We then convert this single letter (a character) to uppercase. The cFirstLetter variable now contains B.

Notice that we have to grab a single character from our response. We use the Chars method to indicate which character we want to evaluate. We then cast it to a CChar (Cast to CHARacter) and assign it a variable of data type Char.

'-- If we were successful, append a ? to the '-- end and trim any leading blank spaces If (bAddQuestionMark = True) And (sbDrResponse.Length > 5) Then Dim sFinalResponse As String Dim cFirstLetter As Char = CChar(sbDrResponse.Chars(0)) '-- Extract the first letter and capitalize cFirstLetter = cFirstLetter.ToUpper(cFirstLetter)

Using the handy Remove and Insert methods of the StringBuilder data type, we remove the first character of the doctor's response. The first parameter of the Remove method, 0, tells the method where to start removing. The second parameter, 1, tells the method how many characters to remove. The Insert method also takes two parameters. The first indicates where to insert, and the second what to insert. With these two lines we removed the lowercase b and inserted an uppercase B.

'-- Remove the first letter and replace it with cap sbDrResponse.Remove(0, 1) sbDrResponse.Insert(0, cFirstLetter)

Now, because we've successfully translated the patient's response to a question, let's add a question mark to the end. Finally, we convert the StringBuilder data type to a string and assign it to the module-level variable sDoctorResponse. We then return True to alert the calling function that we were successful. The variable sDoctorResponse now contains "Because you are depressed?" Of course, if no words in the patient's comment are found in the aTranslate array, we return False from the function.

 '-- Add a ? and return the string sbDrResponse.Append("?") sDoctorResponse = sbDrResponse.ToString Return True Else '-- No luck, return an empty string Return False End If End Function

You can see why we selected a StringBuilder data type for building the sbDrResponse in this function. We essentially built the response on the fly. Using the Append, Remove, and Insert methods we constructed the response. The StringBuilder does all of this in a memory buffer. If we did this with a String data type, any change would result in the string being destroyed and a new one constructed reflecting the changes. Finally, we used the ToString conversion to assign the final product to the string response that will be sent back to the program using this module.

Return a Previous Patient Phrase

In the event that none of the first three functions returns a doctor response, the program tries the getRandomPhrase route. Under certain conditions, this routine replies with a comment made previously by the patient and requests more information about it. This routine not only gives the patient a sense that Eliza has a memory but that the program can see patterns between what was just asked and what was asked several questions ago. This feature is very impressive to the user.

To accomplish this feat, we use our old friend the Mod operator. You can see that this function is used rarely. First, all other functions called before must have failed, and when getRandomPhrase is called, a previous patient response is returned only when the number of responses is evenly divisible by 6. So this does not occur often, but when it does the patient will find it impressive.

We set the iLimit variable to the number of patient responses so far. We also introduce a new class, Random. This class represents a pseudo-random number generator, which means that this class produces a sequence of numbers that meet certain statistical requirements for randomness. Pseudo-random numbers are chosen with equal probability from a finite set of numbers. The numbers chosen are not completely random because a definite mathematical algorithm is used to select them, but they are sufficiently random for our purposes. Finally, we also use a StringBuilder object to construct our response because we need to do a bit of appending.

Private Function getRandomPhrase() As Boolean If (iPatientResponseCount Mod 6 = 0) Then Dim iLimit As Integer = cPatientsResponses.Count Dim iPickResponse As Integer = 0 Dim iRandom As New Random() Dim sPreviousComment As New StringBuilder() 'Be sure to 'use new Dim sComment As String

The Random class is overloaded and returns a random number between 0.0 and 1.0. However, we can alter the range by means of the Next method, which takes two parameters: lower limit and upper limit. This method returns a random integer within the specified range. Notice that we are generating a random number between 0 and iLimit - 1. As you know, cPatientsResponses is a StringCollection. While the string collection might contain eight responses, the first is in position 0. So while the Count property assigned to iLimit will be 8, the strings are in position 0 through 7. If we used iLimit, every once in a while, Random would generate iLimit, which would be one beyond the last legitimate entry, throwing an exception. This exception is not a good thing, so let's anticipate it.

'-- Which random response to select from the '-- cPatientsResponses collection? iPickResponse = iRandom.Next(0, iLimit - 1)

As a double check we ensure that the number of the string to retrieve is less than the Count property of the StringCollection. Because each of the patient's responses is stored as lowercase characters, we want to capitalize the first character. We are familiar with how do to this now. First we assign the string from cPatientsResponses to a StringCollection. Because we are going to manipulate sComment, you might be wondering why we didn't use a StringBuilder. Well, you can't cast a string (from the StringCollection) to a variable of type StringBuilder. Yes, you could if you turned off Option Strict, but we elected not to do that, so we dimension sComment as a string as well to make the assignment cleanly. We then grab the first character of the random previous patient response and capitalize it.

If (iPickResponse <= cPatientsResponses.Count) Then Dim cFirstLetter As Char '-- Retrieve the comment to display sComment = cPatientsResponses(iPickResponse) '-- Take the first letter of the retrieved comment cFirstLetter = CChar(sComment.Chars(0)) '-- Capitalize it cFirstLetter = cFirstLetter.ToUpper(cFirstLetter)

I also wanted to use a string to point out another "gotcha." Recall that when we used the StringBuilder to remove the first character and insert the capital letter, we could simply do this:

'-- Remove the first letter and replace it with cap sbDrResponse.Remove(0, 1) sbDrResponse.Insert(0, cFirstLetter)

Because sbDrResponse is a StringBuilder data type, the conversion is done in a memory buffer. There is no need to assign the string to another variable to accomplish the conversion. But this is not the case with a string! You could use the same code above for a string—it will compile fine—however, the changes are simply ignored! We know that when changes are made to a string, the original is destroyed and a new one created. Because this is the case, when working with a string you must assign any Remove or Insert changes to another string.

We remove the current lowercase first character of the random response and replace it with the uppercase character. Because it's a string, we must assign the change to another string. Behind the scenes, the original sComment is automatically destroyed and a new modified string is created.

'-- Remove the first letter and replace it with cap sComment = sComment.Remove(0, 1) sComment = sComment.Insert(0, cFirstLetter)

We want to embed the patient's previous question in quotation marks, and the way to show a quotation mark literal is to have double quotes (""). Double quotes show up as a quotation mark. Notice that the two Append lines each have four quotation marks. The first line has a quotation mark at the beginning, one at the end, and two together. Next we append a previous response from the StringBuilder and concatenate another quote. This new material is then assigned to the doctor's response and the function returns True. If the conditions are not met, the function returns False.

 '-- Now construct the string for the Dr response -- sPreviousComment.Append("Please tell me more about: """) sPreviousComment.Append(sComment & """") sDoctorResponse = sPreviousComment.ToString Return True End If End If Return False End Function

Notice that not only does this routine return a random previous patient comment, but it capitalizes the first letter and places the entire comment in quotes, as you can see in Figure 6-26.

Figure 6-26

Eliza sometimes displays a previous comment in quotation marks.

When All Else Fails

If each of the four previous functions fails to return an incredibly insightful comment, we resort to returning one of our stock phrases. We need a way to keep track of which current stock response the doctor used last so that we can always send a new one. To do this, we need to dimension a variable that keeps track of the last stock phrase that was sent. One way to do this is to declare a module-level variable that stays in scope as long as the program is running. A better way is to declare a local variable that does the same thing.

We know that all local variables come to life when the procedure is entered and are destroyed when the procedure is exited. When we need a local variable that persists from call to call in a procedure, declaring it as Static does the trick. This special local variable will retain its value as long as the module is in scope. We declare an integer variable as Static and initialize it to 0. When it gets incremented or changed in the getStockPhrase procedure, it retains its value.

Private Function getStockPhrase() As String Static iCurrentStock As Integer = 0

The next section of code might look a bit strange. The first time the function is entered, the value of iCurrentStock equals 0. Because 0 is less than the upper bound of the asStockResponse array, the Else clause is executed. The variable iCurrentStock is incremented and now equals 1. The response in the first array element (i.e., 1 - 1) is returned.

This incrementing continues until iCurrentStock is larger than the upper bound. When that happens, iCurrentStock is set back to 1. We then send back 1 - 0 or the first response all over again. We do this to speed up the code. Remember that when the Return keyword is executed, control immediately returns to the caller. There would be nowhere to update iCurrentStock after the If...Else statement. We would have to increment the counter after we selected a response. This means declaring another temporary variable, assigning the response to it, incrementing the counter, and then returning the response. This was simply a design decision.

 If iCurrentStock > asStockResponse.GetUpperBound(0) Then iCurrentStock = asStockResponse.GetLowerBound(0) + 1 Return asStockResponse(iCurrentStock - 1) Else iCurrentStock += 1 Return asStockResponse(iCurrentStock - 1) End If End Function End Module

The Dialog.vb module is now complete. Ready to call it from the form?

Calling the Module from the Form

Open up the form you created earlier as the interface. Add the following Imports statements to the top of the form as usual, along with an Imports statement for the new Eliza dialog box.

Imports System.ComponentModel Imports System.Drawing Imports System.Windows.Forms Imports Microsoft.VisualBasic.ControlChars Imports Eliza.Dialog

Now set the lblDoctor.Text property, which will be displayed each time Eliza is run.

Public Sub New() MyBase.New() 'This call is required by the Windows Form Designer. InitializeComponent() 'Add any initialization after the ' InitializeComponent() call

 lblDoctor().Text = "What would you like to talk " & _ "about this beautiful day?" End Sub

Now add the KeyDown event procedure for our txtQuestion text box. What we do is check each key press and determine whether the Enter key was pressed. When you want to check or restrict text from being entered in a text box control, create an event handler for the KeyDown event. This will permit you to validate each character entered in the control.

We added Imports Microsoft.VisualBasic.ControlChars so that we could easily check for when the Enter key is pressed. The key pressed by the user is in the KeyEventArgs parameter passed into the event handler. All we have to do is see whether the key code is equal to the Enter key. If it is not (in most cases), the event handler is simply exited.

If, however, the Enter key was pressed, we know the patient is submitting a response to the good doctor. In that case, we pass in the contents of the txtQuestion text box as a parameter to the getElizaResponse method of the Dialog module. The value returned is displayed in the lblDoctor label on the interface.

An important design feature to note is how we totally decoupled our module from the interface. The interface does not have to know anything about the module. It does not have to initialize it or understand how it is implemented. To the interface, our module is a black box. A patient response is passed in and a doctor response is passed back. This feature is an important point that you should strive to emulate in your designs.

Private Sub txtQuestion_KeyDown(ByVal sender As System.Object, _     ByVal e As System.Windows.Forms.KeyEventArgs) _     Handles txtQuestion.KeyDown     '-- If the user doesn't hit Enter, they are not finished     If (Not e.KeyCode.Equals(keys.Enter)) Then         Exit Sub     Else         lblDoctor().Text = _             Dialog.getElizaResponse(txtQuestion().Text)     End If     txtQuestion().Text = "" End Sub

Also, when we go to hook up the dialog box in our user interface, you can see the benefit of making each of the member variables and procedures private. The user only gets to see the single interface—getElizaResponse, as you can see in Figure 6-27. This makes our module pretty foolproof to use.

Figure 6-27

The module's private interfaces are not exposed.

And here is where the strong typing of Visual Basic .NET comes into its own. When the user hooks up the module, it's easy to see that it is expecting a patient response of type String and that it will return the favor by passing back a string. No ambiguity here, as you can see in Figure 6-28.

Figure 6-28

Strong typing lets IntelliSense know exactly what parameter types a function expects.



Coding Techniques for Microsoft Visual Basic. NET
Coding Techniques for Microsoft Visual Basic .NET
ISBN: 0735612544
EAN: 2147483647
Year: 2002
Pages: 123
Authors: John Connell

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net