Contents

3.5 SCROLL_TERMINAL package

The SCROLL_TERMINAL is a portable, general purpose interface. It is called "scroll" because new data is printed at the bottom of the screen, causing old data to scroll off the top.

3.5.1 Reusability and Consistency

The user interface is often one of the most complicated parts of an application program. That makes it an important candidate for reuse. After all, isn't it better to reuse the components that are hardest to write, than to reuse trivial ones?

Auto manufacturers recognize that people are creatures of habit, so they put controls where people expect them to be. Maybe the steering column isn't the best place for the turn signal, but any auto maker who moved it to a "better" place would have trouble selling cars. Unfortunately, many programmers don't have the good sense auto makers do. Every time they write a program they redesign the user interface. Sometimes they use TAB to advance to the next item on a menu, other times they use the RIGHT arrow key to do that. Sometimes backspace acts as a delete, other times it doesn't. It can drive a user nuts.

Windows are becoming popular and are starting to put an end to unique interfaces. When application programs use commercially available window products they not only benefit by reusing tested user interfaces, they also present a familiar interface to the user. A complete window package with graphics and a mouse interface is too complicated to be included in a book of intermediate Ada examples, but SCROLL_TERMINAL gives you the same benefits of reuse and consistency. It is a simple, standard user interface which can be used by a variety of application programs.

3.5.2 Layering

Often it is a good idea to build device handlers layer upon layer. That's what I've done here. The SCROLL_TERMINAL (Listing 22) is built on the VIRTUAL_TERMINAL, which hides hardware specific differences. There is no need for special SCROLL_TERMINAL bodies for each of the different implementations. The SCROLL_TERMINAL has the same number of lines and columns as the VIRTUAL_TERMINAL, and propagates the VIRTUAL_TERMINAL.PANIC exception by renaming it. Therefore, the application program doesn't need to know that there is a VIRTUAL_TERMINAL under the SCROLL_TERMINAL.

3.5.3 SCROLL_TERMINAL Features

The SCROLL_TERMINAL boasts features you won't find in TEXT_IO. It lets you check the keyboard to see if the user has entered any keystrokes, and you can flush the typeahead buffer to discard any entries that may have been typed (but not processed) before the prompt was displayed. You can turn the character echo on or off. There are several new get procedures which return strings of the proper length (padding with blanks if necessary). The new input routines include a prompt, like a BASIC input statement. (If you don't want a prompt, then use the null string as a prompt.)

The SCROLL_TERMINAL has an interesting way of handling default responses. It works like this: The screen shows the prompt and the default response. If you press RETURN, it takes the default. If you begin typing a new response, the default is automatically erased. If the default is almost exactly what you want, you can edit the default response.

The LEFT and RIGHT arrow keys can be used to move the cursor without changing the characters underneath the cursor. If the keyboard doesn't have arrow keys, CONTROL-L and CONTROL-R can be used as LEFT and RIGHT arrow keys. BACKSPACE (CONTROL-H) deletes the character to the left of the cursor and moves the cursor back one space. DELETE (CONTROL-E) erases the character covered by the cursor. INSERT (CONTROL-A) adds characters at the cursor location without destroying any existing text.

Whenever you press RETURN, the entire response showing on the screen is taken, regardless of the cursor position. (You don't just get the characters to the left of the cursor.) This was done at the request of a customer who insisted on a constant policy of "What you see is what you get."

The SCROLL_TERMINAL also generates an exception called NEEDS_HELP whenever the user presses the question mark key. This is an important feature you will see demonstrated often in the later programming examples.

3.5.4 Compatibility

Even though I don't like TEXT_IO, I know there are a lot of application programs that have already been written using TEXT_IO as the user interface. Therefore, I should make it as easy as possible for those programs to use SCROLL_TERMINAL instead of TEXT_IO. The routines with TEXT_IO names (put, put_line, new_page, get_line, and so on) are identical to TEXT_IO so existing programs that use TEXT_IO for a user interface can use SCROLL_TERMINAL instead simply by substituting with SCROLL_TERMINAL; use SCROLL_Terminal for with TEXT_IO; use TEXT_IO;.

3.5.5 Hiding Details in the Package Body

The package body is shown in Listing 23. It makes a distinction between cursor positions and column numbers. The column can be 1-79 but the cursor can be at position 1-80. This was done so the cursor will always be to the right of the character just entered. The application program doesn't need to know about Cursor_positions, so this data type is hidden in the package body.

3.5.6 Coupling

If one module affects another module, those two modules are said to be coupled. If modules are too tightly coupled, then there are major maintenance problems. When you change one module it forces you to change another, which is coupled to another so that other module must also be changed, and the ramification of a change ripple through the whole system. Too much coupling is bad.

If you take the extreme position that all coupling is bad, then you can never build a working system because all the modules in a system have to work together to achieve the goal. They can't do that if they are completely independent.

The coupling quandary is like networking computers. Many people want their computers to be networked (coupled) together so they can share data, but as soon as they do that they run risks. A network failure could keep them from accessing vital data. A hacker could break into one part of the network and gain access to the whole network. When networking computers you want to control the coupling, so authorized users can safely pass data (even if part of the network fails), but unauthorized users can't get at any of the data. The same is true of coupling software modules. You need controlled coupling.

One of the best ways to control coupling between modules is to pass all information from one to the other using parameter lists. This method allows Ada to check for consistency between modules, and helps programmers see how modules are coupled. I pass information between modules using parameter lists whenever I can, but there are some times when this isn't practical. The SCROLL_TERMINAL body is a good example.

There are three variables in the package body that are used to couple several routines together. COLUMN_NUMBER tells where the cursor is. TAB_STOPS remembers how many columns there are between tab stops. ECHO is used to decide if input characters should be echoed or not.

Let's look at ECHO first. All of the input procedures need to know if they should echo characters to the screen as they are typed. We could do that by adding one more parameter to all of the input procedures. This boolean parameter would tell the input routine if it should echo or not. You could put this parameter last in the list, and give it a default value of TRUE, and it wouldn't be too awkward, but you would force every application program that ever needs to control the echo to keep track of the echo status itself.

I elected to add Echo_On and Echo_Off procedures to the SCROLL_TERMINAL body to make it easier on the application program. Since the normal mode is Echo_On, the elaboration of SCROLL_TERMINAL always calls Echo_On. The application program can call Echo_On or Echo_Off whenever it wants, and does not need to remember the current echo status.

The method works by coupling the Echo_On and Echo_Off procedures to all the get procedures, using the shared variable ECHO. In so doing, I intentionally violated a software quality assurance guideline that says, "There shall be no hidden couples between modules." If management got really nasty about it, I could get rid of the ECHO variable and clutter all the input routines with boolean parameters, without doing too much damage to the overall design.

The situation isn't so simple with COLUMN_NUMBER and TAB_STOPS. These variables provide a straight-forward (hidden) way of coupling the output procedures. Look at what has to happen.

The procedure put has to do more than just output characters. It also has to keep track of the cursor position. It has to do this so it can expand TAB characters. If the character to be printed is a printable character, it generally prints the character and moves the cursor to the next location. The one exception is when the cursor is at column 80. Then the new_line procedure is called and the character is printed in the first column of the next line, and the cursor moves to column 2.

Non-printable characters are handled specially. CARRIAGE_RETURN sets the cursor and the COLUMN_NUMBER back to column 1. LINE_FEED and BELL are sent to the display without affecting the COLUMN_NUMBER. TAB is sent as one or more spaces (until COLUMN_NUMBER is a multiple of TAB_STOPS). Other non-printable characters (ESCAPE, for example) are replace by BELL characters to prevent programmers from trying to send terminal-specific control sequences to the screen. (The SCROLL_TERMINAL is a portable package. If it lets terminal-specific codes through, then you can't be sure an application program can be ported to another system.) The procedure Set_Col acts like a TAB, except it spaces over to the specified column regardless of TAB_STOPS.

So Set_Tab, Set_Col, new_line, and put are all coupled through COLUMN_NUMBER and/or TAB_STOPS. I'm a really clever fellow, so I probably could figure out a way to pass COLUMN_NUMBER and TAB_STOPS all over the place using parameter lists, but what a maintenance nightmare that would be!

The guideline is correct. You should generally avoid using shared variables as hidden couples between modules. The guideline shouldn't be considered to be absolute. There are times when the guideline should be violated. Some would argue that the SCROLL_TERMINAL body is a module, and that Set_Tab, Set_Col, new_line, and put are cohesive parts of one module, so the guideline hasn't been violated. That's just avoiding the issue. You have to recognize that there are rare instances when it is better to use hidden coupling than visible coupling.

3.5.7 Module Partitioning

The SCROLL_TERMINAL package body is broken into two files (Listings 23 and 24). The first file contains the main part of the package body, and the second file is the Get_Response subunit. The package body is five pages long, and the subunit is five pages long. If I left Get_Response in the package body, then the body would have been ten pages long, and that is too long.

Some people would say that even five pages is too long, and I generally agree with them. I try to keep each compilation unit to three pages or less, but I don't have a fixed rule, "Thou shalt not write any compilation unit longer than three pages." Guidelines have to be tempered by common sense.

I could have reduced the size of the SCROLL_TERMINAL body by turning put_line, New_Page, Set_Col, Set_Tabs, and several other small procedures into separate units. That might have made the body small enough to satisfy someone's software quality assurance guideline, but would that really be an improvement? Does it really help to break SCROLL_TERMINAL up into a dozen files, many of which contain procedures with only six statements? The file headers would take up more space than the executable code! (That's not a disk space problem, it is a visual clutter problem. The code gets lost in the boilerplate "information" people unconsciously skip over.)

I'm not arguing in favor of big compilation units. There are good reasons for keeping compilation units small. When compilation units get large, it is hard to find a particular piece of code. Often a module is too large because it is trying to do too many things at once, and should be broken down into several smaller modules that each do a single thing. Whenever you have a module over several pages long, you should seriously consider dividing it into smaller pieces.

Partitioning the code into smaller modules is a good idea most of the time, but there comes a point where partitioning hurts more than helps. I have friends working on a project where management insists that every subprogram must be a separate module. If I wrote SCROLL_TERMINAL for that customer I would have to treat Set_Tabs as a configuration controlled module, complete with pseudocode description, structure chart, data dictionary, formal walkthrough, requirements traceability, module test plan, and integration test plan. (My friends' project is currently one year behind schedule, about to announce another year schedule slip, is way over budget, with no end in sight. I think I know why.)

3.5.8 Limited Name Space

Another factor you need to consider when partitioning a program into modules is that the more modules you have, the more module names you need to make up. You may think I'm joking, but when you have a big program with several hundred modules, it gets tough to find a name that is short, meaningful, and hasn't already been used. This is a serious problem in Ada because subunit names can't be overloaded.

The SCROLL_TERMINAL body has two overloaded subprograms called put. One put takes a character as a parameter, the other takes a string. Both exist blissfully in the package body. I can make either one of them a subunit, but if I try to make both of them separately compilable subunits, I can't do it. I would have to rename one, hide one in yet another package, or think of something else equally clumsy. Sometimes it may be better to let a module get a little bigger than the guidelines suggest, rather than be forced to do something creative to a procedure name to resolve a name clash so you can use separate subunits.

The SCROLL_TERMINAL body and Get_Response subunit are long, but I don't think they would be improved any by more partitioning. Partitioning would just make configuration management more difficult because you would have so many more files with strangely named modules in them.

3.5.9 The Top is at the Bottom

Ada makes it easy to write programs from the top down, but when you are finished writing it you will find the top level logic is at the bottom of the listing. You can see this when you look at the Get_Response subunit.

Ada needs to have all the minute details described to her before she is willing to look at the big picture. In the Get_Response subunit she needs to be told all about Beep, Forward, Backup, and so on, before she will even consider the main sequence of statements. My human mind balks at this. I can't help thinking, "Why are you telling me about this Beep? What has Forward got to do with getting a response from the user?" I find it much easier to understand a listing by going to the end and working backwards.

With this in mind, lets skip all the way down to the main begin statement in Listing 24. The program begins by setting the INSERT_MODE to FALSE. This means characters entered will type over existing ones, rather than shoving existing characters to the right to make room for a new character to be inserted at the cursor location. Then the DEFAULT response is put in a temporary BUFFER, and the BUFFER is displayed if the ECHO is enabled. If ECHO is disabled, then blanks are displayed instead. Writing the contents of the BUFFER (or blank spaces) to the screen moves the cursor to the end of the BUFFER. X is a scratch variable used to keep track of where the cursor is, so it is set to the end of the BUFFER. Then the Backup procedure is called as often as necessary to move the cursor back to the beginning of the BUFFER. Backup automatically adjusts X and the COLUMN_NUMBER.

All of these actions happen in the twinkling of an eye. You probably won't see them unless you look closely at the screen, or are using a 1200 baud modem. To the casual observer, the default appears and the cursor is sitting at the beginning of the default. The program is now sitting at the VIRTUAL_TERMINAL.get(C); line, waiting for the user to enter a character.

The first character the user enters may be special. If the user presses INSERT (CONTROL_A), the INSERT_MODE is set to TRUE. This allows subsequent characters to be added to the beginning of the default. If the first character entered is DELETE (CONTROL_E), the first character of the default is erased but the remainder of the default remains. If the first character is a RIGHT arrow (CONTROL_R), the cursor moves one space to the right, without affecting the default characters under the cursor. If the first character is the RETURN key, the DEFAULT response (visible or not) is returned as the user's input and the subunit is done.

Often the first character will be none of the above. In that case, the default response is erased and the first character is processed as a normal keystroke input. The default is erased by filling the BUFFER with blanks and writing the BUFFER to the screen. This puts the cursor at the end of the BUFFER again, so Backup needs to be called to move the cursor back to the beginning of the BUFFER. Since no characters have been processed yet, SIZE (the number of valid characters in the buffer) is set to 0.

After the initial character is entered, the program goes into a loop that gets a character and processes it. The Process_Character procedure puts the keystrokes in the BUFFER and sets DONE to TRUE when the user presses RETURN. The DONE flag is used to exit the loop. When this happens the BUFFER and the SIZE are passed back to the main program and the cursor is moved to the beginning of the next line.

That is the top level description of the logic flow. It gives a general description of what Get_Response does. Suppose we want more detail about the Process_Character routine. We go to the next lower level by backing up in the listing.

The Process_Character procedure is found just before the main begin. Process_Character is always called after the user has entered the character C. A case structure decides what to do with C. For example, if the C was the RETURN key, then all that needs to be done is to set DONE to TRUE. If it was a BACKSPACE character, then Rubout the character to the left of the cursor.

If you backup again and look at the Rubout procedure you can see that it checks to make sure there really is a character to the left of the cursor. If so, it calls Remove to remove the character from the buffer. If not, it just makes the terminal Beep at the user so all his coworkers will know he did something stupid.

Suppose someone gave you the Get_Response subunit and asked you to draw a picture of it, using your favorite form of structure chart. The top level diagram would show what happens in the main sequence of statements. One of the items drawn under Get_Response would be Process_Character. If you drew a diagram of Process_Character, it would have Rubout hanging from it. If you drew a diagram of Rubout it would have Remove and Beep under it.

In general, the closer you get to the top of the listing, the closer you get to the bottom of the structure chart. This even extends to the context clauses because they name utility packages and subprograms that appear at the very bottom of the structure chart (if they appear at all).


Contents | Next ...