The FORM_TERMINAL package is more sophisticated than the SCROLL_TERMINAL. In some instances it is a much easier interface for the user to use, but it puts a little more burden on the programmer. This is an unavoidable consequence of the law of conservation of energy. It takes a certain amount of work to get a job done. Some of the work has to be done by the user, and some has to be done by the programmer. The more work the programmer does, the less the user has to do, and vice versa. A programmer has to work hard to come up with an easy user interface.
The complete FORM_TERMINAL consists of nineteen listings. That's too much to tackle at once, so lets begin by looking at the first six listings (25 through 30). They provide most of the functionality of the FORM_TERMINAL. The last thirteen listings (31 through 43) are only used to create and modify forms, and will be discussed later.
I saw another excellent example of the good use for the FORM_TERMINAL when I went to see my insurance agent a few days ago. I wanted to make some changes on my car insurance. He sat down at his computer and typed in my name, and a screen full of information appeared. It showed my name, address, birthday, vehicle type, coverage limits, and who knows what else. The agent was able to move the cursor to the proper field, make the change to the coverage I wanted, and pressed a button. The computer figured my new premium. If he had used the SCROLL_TERMINAL to do that he would have had to have reentered my name, address, birthday, and so on. It would have been a real pain.
All of the keys that work for SCROLL_TERMINAL work exactly the same way for the FORM_TERMINAL. The FORM_TERMINAL also uses four more keys. The UP and DOWN arrows mean "previous form" and "next form." The TAB and BACK_TAB keys mean "next field" and "previous field."
It was tempting to use some of the IBM PC keys, like "Pg Up", "Pg Dn", "Home", and "End" for the FORM_TERMINAL. I choose not to because some terminals, like the Televideo 910, don't have these keys. I wanted to keep the mental re- mapping of keys down to a minimum. I didn't want to have to remember that function key F5 is Pg Dn on a Televideo 910.
No matter what hardware is used, the FORM_TERMINAL always works the same, because it is built on the VIRTUAL_TERMINAL. No modifications are required to port it to an environment that already has the VIRTUAL_TERMINAL package running. That's good, because the FORM_TERMINAL is extensive, and makes heavy use of cursor control keys. It would be difficult to rehost if it was built on host operating system calls.
I'm sure you've had experience with all kinds of forms. For example, income tax forms, employment applications, an application for a driver's license, and so on. What do all these forms have in common?
First, they all have a limited size. There are only so many characters you can fit on a single page. If all the information won't fit on a single page, you need a multiple- page form. In the FORM_TERMINAL package specification (Listing 25), I've defined the size of a single page with the subtypes Line_numbers and Column_numbers. This size is related to the size of the terminal screen because you need to be able to see the whole page all at once. I've made the form one line shorter than the number of lines on the screen so I can use the bottom line for status and instructions that aren't part of the form. It may be that you can't fit all the information you need on a single form, but that happens with paper forms, too. The solution is the same for the electronic form. Use more pages if necessary.
The second thing you notice about a form is that it is divided into parts. Some of these parts have information preprinted on them. Other parts are blank so the user can enter data. The FORM_TERMINAL calls these parts "fields." Protected fields are those printed parts the user can't change. Unprotected fields are the blanks he can fill in. Unlike paper forms, this electronic form can have a default response in an unprotected field.
Paper forms sometimes have numbered fields. That's because you sometimes need to refer to a particular field. In version 1, I numbered the fields on the form, but I found it hard to remember what numbers referred to each field. In version 2, I chose to give every field a 20-character name. The FORM_TERMINAL uses a subtype Field_names to represent the names of the fields.
Clearly there will be at least two things we will want to do with these fields. We will want to print instructions and default responses in some of them, and we will want to read what the user has written in the blanks. The get and put procedures allow us to do this. We can put a text string to any field we name, or we can get a string from any named field.
There are other, not so obvious, things we need to do with a form. One is to simply display it. More often we want to display the form and give the user a chance to update the information on it. The Display and Update procedures let us do that.
The two parameters CURSOR_AT and NEXT in the Update procedure need a little explanation. CURSOR_AT lets us place the cursor in any unprotected field. This is the field we expect the user to want to change first. Generally it will be the first unprotected field in the upper left corner of the form. Each time the user presses the RETURN or TAB key, the cursor will move to the next unprotected field. If the user presses the BACK_TAB key, the cursor moves to the previous unprotected field. The cursor will never appear in a protected field because protected fields contain things the user is not allowed to change.
When the user moves the cursor off the bottom of the screen (using the RETURN or TAB key in the last field, or using the DOWN arrow anywhere on the form), the Update procedure knows the user is finished with this form and wants to go on to the next page (if any). When this happens, the NEXT parameter is TRUE. It may be, however, that the user wants to go back to a previous page. He can do this by pressing the UP arrow, or using the BACK_TAB key until he moves the cursor off the top of the page. In this case Update returns with the NEXT parameter set to FALSE. This should be interpreted as a request to go back to the previous form and update it again.
This idea of multiple pages brings up an interesting design decision. How do you keep track of multiple page forms? In version 1, I had a limited private type called Forms and declared arrays of Forms. All the Forms were in memory at once, and I could move forward and backward by simply incrementing or decrementing the index. The problem was that I was using a less efficient internal representation of the form than I use now, and I could only get three forms in memory at once. Furthermore, version 1 of the Alsys compiler didn't realize it didn't have enough memory for a fourth form, and wrote it over my code.
The representation of forms used in version 2 (although the most compact form possible) takes much less space than version 1 needed, so I suspect I could hold as many forms in memory as necessary under normal circumstances. I was afraid, though, that someday I would have an application that required more forms than there was room for. This could happen because I needed lots of forms, or because the code segment of the program used up almost all of the memory space. I decided it was safer to keep just one form in memory at a time, and keep the others on disk. (This has a side benefit. If there is a power failure I lose only the information on the form currently being updated.)
The decision to store forms on disk required Read and Write procedures to be included in the package. This means the application program needs to know the names of the files containing the forms. I have a generic FILE_SYSTEM package that hides path names and makes it easy to port programs to different operating systems, but it is too complicated to include in an intermediate level book. It is not necessary to use a special file system, however. You can use a simple file name. (You will see how this is done in a later figure.)
There are several things that could go wrong when using the FORM_TERMINAL. You might try to read a file that doesn't exist, write a form to a full disk, get a form from a file that doesn't exist, or read a form from a file that doesn't really contain a form. The exceptions READ_ERROR, WRITE_ERROR, ASSIGNMENT_ERROR, and LAYOUT_ERROR are raised in these situations. With the exception of WRITE_ERROR (disk full), these exceptions probably won't happen after you have debugged your application program.
There are two other exceptions that could happen under normal circumstances. These are PANIC and NEEDS_HELP. Both of these exceptions are raised at the user's whim, and no amount of debugging can prevent them. The PANIC exception is raised whenever the user says to himself, "Oops! I didn't want to do this. Let's quit." The NEEDS_HELP exception is raised whenever the user presses the question-mark key because he doesn't know how to answer the question. The NEEDS_HELP situation is a classic example of a puzzling problem to many new Ada programmers, so let's digress for a moment and talk about it.
People who don't know how to use Ada exceptions usually find themselves in the dilemma shown in Figure 21. This example lets the user enter a name and address and echoes it back. There is a possibility that the user will raise the NEEDS_HELP exception by pressing the question mark key. When this happens, the dilemma is that the NEEDS_HELP exception doesn't tell us which of the three questions confused the user, and we couldn't get back to that question even if we knew where we wanted to go.
The usual solution is to structure the program as shown in Figure 22. By encapsulating each query in a separate procedure it is possible to separate the exception handlers. Each exception handler gives an appropriate help message and then recursively calls the appropriate input routine. This is a good solution because it keeps the exception handler close to the point where the exception will be raised, and it keeps the error routines from cluttering the main program. I recommend using this solution whenever possible.
Unfortunately there are cases where this solution won't work. We are faced with such a situation when we use the FORM_TERMINAL instead of the SCROLL_TERMINAL to get the name and address. Consider the Form_Dilemma shown in Figure 23. ADDRESS.DAT is a file containing the data necessary for drawing a simple name and address form on the screen. We will look at the contents of this file in a few pages. Right now all you need to know is that it defines a form that prompts for name, address, and city, and tells where these fields should appear on the screen. Form_Dilemma fetches the blank form from ADDRESS.DAT, lets the user update it, extracts the name and address from the form, and echoes it to the screen. It works fine, unless the user NEEDS_HELP. Then we are back to another variation of the usual dilemma. We are in the exception handler, don't know why, and don't know how to get back. What are we to do?
A horrible solution is shown in Figure 24. We will see a much better way to solve the problem in a moment, but we must suffer through this bad example just to see what's wrong with it. I call this the FORTRAN_Mentality_Solution because FORTRAN teaches people to program this way. Some Ada critics claim you HAVE to solve the problem this way. If that was true, they would be justified in their criticism. But let's not damn Ada for their ignorance.
The FORTRAN_Mentality_Solution is to rewrite Update somehow to eliminate the NEEDS_HELP exception and replace it with a STATUS variable. Every time the procedure is called you must check the STATUS variable to see if the procedure completed correctly. There are two problems with this. First, it forces you to depend upon every application programmer who will ever use the Update procedure to remember (or care enough) to check the STATUS variable. Second, it adds overhead every time you use it, to make sure the procedure completed correctly.
The FORTRAN_Mentality_Solution requires several GOTO statements. Ada doesn't normally need GOTOs. The GOTO is included in the LRM for no other reason than to allow you to convert poorly structured FORTRAN into poorly structured Ada. That's what I've done here. Please don't consider this an endorsement of GOTOs. Remember, this is the wrong way to solve the problem.
The right way to solve the problem is shown in Figure 25. It is similar to the usual solution because it uses a block structure to encapsulate a routine that is likely to raise an exception, and provides a local exception handler for the routine. This eliminates the need to go back to the point of the exception because we haven't really left the point of the exception. In this case, that's only half the solution. We still have to figure out why NEEDS_HELP was raised.
Ada doesn't provide any way for me to pass the WORKING_FIELD number back along with the exception. Even if she did, it wouldn't do me much good because the application program thinks in terms of the names of the fields, doesn't know the fields are in an array indexed by an integer, and doesn't know WORKING_FIELD is the index. (If I let the application programs know this, I don't dare ever change the representation of a FORM for fear it will mess up a critical application program someone else wrote.)
Application programs using the FORM_TERMINAL will know about Field_names because they use them to get and put data, and position the cursor. The application programmer needs to know the name of the field being processed when the NEEDS_HELP exception raised. The real key to the solution is having the foresight to include the Confusing_Field function in the FORM_TERMINAL package. Whenever the NEEDS_HELP exception is raised, the application program can call the Confusing_Field function to find out the name of the field that was being processed when the exception was raised. This is similar to checking a status variable, but the difference is that you only do it on those rare occasions when the exception occurs.
To do this I had to move the WORKING_FIELD number out of Update (where nobody but Update can use it) and put it in the FORM_TERMINAL body. Here other FORM_TERMINAL subprograms have access to it. The WORKING_FIELD variable is shared by Update and Confusing_Field. Update reads and writes it. Confusing_Field reads it and converts it to the corresponding NAME, which is what the application program needs to know.
The Confusing_Field function not only gives the application program all the information necessary to handle the exception, it also leaves me free to change the internal representation of the FORM. Suppose there was a compelling reason for me to change to a linked list (which uses an access type FIELD_POINTER instead of the integer WORKING_FIELD) to represent a FORM. I could do this with full assurance that I wouldn't have to rewrite any application programs that use FORM_TERMINAL, providing I put FIELD_POINTER in the package body, and I modify the Confusing_Field function to convert FIELD_POINTER to a field name.
There is another reason for keeping the shared variable out of the package specification. If the shared variable is in the package specification, you have lost the ability to change internal representations. Suppose WORKING_FIELD was in the package specification, and you changed to a linked- list scheme that uses FIELD_POINTER. Then every application program that used WORKING_FIELD wouldn't work any more, and would have to be rewritten.
So, the general lesson is this: Whenever you have information you need to pass back to an application program when something goes wrong, store that information in a variable declared in the package body, instead of a subprogram body. Then write another subprogram in that same package that can return the information to the application program in the most useful form.
There are major differences between the two units. You can find them easily using a file comparison utility program. Most of the differences aren't worth much discussion. The FORM_TERMINAL doesn't need to check the ECHO flag because it isn't designed to be used in applications where it shouldn't echo the response. It doesn't have to worry about how many characters the user has entered because the SIZE of the blank of the form is constant. But even though there were significant differences, I still saved time and effort by editing a copy of an existing unit instead of starting from scratch.
The difference that is worth talking about in detail has to do the parameter list. Get_Response passes DEFAULT, TEXT, and LENGTH as parameters. When the user presses the RETURN key he is done, and that's all there is to it. The FORM_TERMINAL is more complicated because the user can end his response by saying he wants to go to the NEXT_FIELD, PREVIOUS_FIELD, NEXT_FORM, or PREVIOUS_FORM. That explains why Get_Form has to return one of those Actions. The parameter list of Get_Form clearly shows this, but it isn't immediately obvious how Get_Form knows what the prompts and defaults are, or how it returns the user's responses.
You don't see me using global variables very often. In this case, however, I feel justified in using them. True, I could pass an object of type Forms, but that seemed awkward. It wouldn't explicitly show the prompts, defaults, and response, so it didn't really provide any more information than using the global variable. Since there is only one object of type Forms, I can't possibly get it confused with any other object of the same type. The application program can't see FORM, so it can't corrupt it.
The FORM is indexed by WORKING_FIELD, which I definitely don't want to pass as a parameter because there is a possibility that an exception might be raised. (If WORKING_FIELD is passed as a parameter I can't guarantee it will be the correct value if NEEDS_HELP is raised.) It seems strange to me to pass FORM as a parameter but index it with a global variable. The exception argument also holds for the FORM itself. If the user NEEDS_HELP, I can be sure the global variable FORM contains the user's partially edited form. If FORM is passed back as a parameter, and the user NEEDS_HELP, there's no telling what is in FORM. Maybe it is the virgin form before the user started editing it. Maybe it is the partially edited form. Maybe it is garbage pointed to by whatever number happened to be in the stack frame when the exception was raised. This is just one of those rare cases where a global variable makes more sense than a passed parameter.
You are probably used to packages that contain objects. This time the package itself is an object. The package represents something that has a value. When you write FORM_TERMINAL.put(SOME_FIELD,"Some Text"); you are actually assigning a value to a component of that object. You can read it back using the statement FORM_TERMINAL.get(SOME_FIELD,STRING_VARIABLE);. That's why you don't need to declare objects of type Forms. The package itself is the form.
Looking at the FIELD component, we see that it is an array of Field_specs, where each specification tells the name of the field, what line it is on, where it begins and ends, and if it is protected or not. The text showing on the form is not part of the field specification. Instead, it is stored in a two dimensional array of characters called a SCREEN. Since many of the characters are blank, there is a potential for saving some space by storing text in the Field_specs and doing away with the SCREEN. (This requires nested discriminated records, and I didn't want to get that complicated.)
Version 1 of the FORM_TERMINAL didn't use a discriminated record. It used an ordinary record, and one component of the record was a fixed length array of Field_specs. A constant MAX_FIELDS set the size of this array. At various times this constant was 30, 60, or 100. When I tried using 30 I ran into trouble because I often wanted more than 30 fields on the form. When I tried 100 fields, it used up so much memory I only had room for one form in memory. I finally settled on 60 as a reasonable compromise, but I always worried it would be too large or too small.
The discriminated record lets me declare an array of fields that is exactly the right size. The problem is that I have to know how many fields will be on the form before I declare it, and I have to change the entire form in one shot if I want to redimension it. That's not a trivial problem, but it is far from impossible. If you have worked with discriminated records before you have probably run into this difficulty. Maybe you gave up. If you did, you'll be glad to see this solution.
For those of you who have not run into this problem, let me quote three pertinent parts of the LRM.
Here are three examples to show what those three parts of the LRM mean in practice. I've written these examples as procedures so you can compile them to see what error messages your compiler generates.
Figure 26 shows that an object must be constrained when you declare it. This constraint can be explicitly shown, as in FIRST_NAME or WHOLE_NAME, or it can be the default, as shown in LAST_NAME. MY_NAME is illegal because it has no explicit constraint and it has no default constraint.
Figure 27 shows that constraints can be changed only when the object was declared with a default constraint. Since LAST_NAME was declared without an explicit constraint, that constraint can be changed. Notice that it is the object declaration, not the type definition, that counts. WHOLE_NAME is of type Default, so it has a default constraint, but we didn't use the default when we declared it. When we declared WHOLE_NAME : Default(14); we told Ada we want WHOLE_NAME to always have a 14 character TEXT string, and she takes us at our word. If we change our mind later, its just tough luck.
The moral of the story so far is, "If you want to be able to change a discriminant of an object, the type definition must include a default value and the object declaration must use that default."
Finally Figure 28 shows us that even under the special circumstance when we can change the discriminant, we may only change it in a particular way. We can't change the discriminant alone, we must change every component of the object at once. The only way to do that is with an assignment statement. We can assign the value of another object of the same type to it, or we can assign an aggregate (but not a slice) to it.
The rest of the lesson is, "You either set the discriminant to the the correct (constant) size when you declare the object, or declare the object without a constraint and change the whole object at once." The two ways to change an object all at once are to assign all the components using an aggregate, or assign it to another object of the same type (even though it has a different constraint).
Of course there was a reason for this long explanation. The FORM_TERMINAL needs to read a discriminated record from a disk file. In general, this means it needs to change the size of a discriminated record currently in memory, and read new data into it. The preceding discussion was designed to impress upon you how tricky this is.
The Read subprogram (Listing 28) opens the input file and reads the first line. This line contains the number of fields in the FORM. Then it calls a function, Stored_Form, passing it the number of fields as a parameter. The function Stored_Form declares and object TEMP of type Forms, with the discriminant set to the proper size. It reads the information from the file a line at a time, and builds up TEMP a piece at a time (but it never changes the discriminant because it was the correct value to begin with). When it is all assembled, TEMP is returned as the result and assigned to FORM all at once. So, reading a discriminated record from a file isn't difficult, if you know how to do it.
Compilers vendors have taken different approaches to implementing discriminated records. You can't depend on every validated Ada compiler to implement discriminated records exactly the same way. This means you can't count on discriminated records to use the minimum required space, and that could cause portability problems if you use discriminated records.
If I had it to do all over again, I might not have used a discriminated record because of their unpredictable nature. I was tempted to rewrite the FORM_TERMINAL using an ordinary record and a generic parameter MAX_FIELDS, or perhaps use access types to build an unbounded linked list of Field_specs. I decided not to because it would have left me without an example of discriminated records.