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.
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.
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.
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.)
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.
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).