jmh@coyote.uucp (John Hughes) (12/20/90)
This is an article I wrote a while back, with the intention of eventually publishing it. I never got around to it, and it has not been throroughly examined by Modula-2 wizards for complete accuracy (other than some comments by Roger Carvalho). I have since moved on to a full- time preoccupation with C and Unix, so my Modula-2 has been on hold for over a year. I hope that some of those folks with questions about the PROC type in Modula-2 will perhaps find it useful. The wizards will hopefully just find it amusing and spare me any major flames. The examples really do compile and run, so I at least got that much of it correct. With that said, here it is: ===================================================================== Using Procedure Vectors To Ease User Interface Design By John M. Hughes - 1 January 1989 Background Developing large software systems with extensive menu-driven interfacing is often an arduous task at best. The common approach of using layer after layer of CASE structures or switch constructs leads to programs that are difficult to maintain, and tedious to design and debug. The programming language Modula-2 offers a simple an elegant way to completely sidestep this aspect of user interface design. The result is a simple one or two procedure deep control structure that is easy to modify and simple to implement. This is accomplished by utilizing the PROC type of Modula-2 and creating vector tables for each menu structure in the system. Some Thoughts On Vector Tables One of the more interesting features of Modula-2 is its ability to declare procedures as types. This, in effect, allows a programmer to write code that behaves much like the indirect operations available with certain processor instruction sets. Vector-tables (also referred to as jump-tables) are well known to those programmers whom spend large amounts of time dealing with assembly language code. The common approach is to build a list of target addresses for various subroutines, and then jump "through" the list based on a relative offset value. This offset value is itself nothing more than a pointer into the list, which contains the addresses of subroutines to be executed with an indirect JSR (jump to subroutine) or CALL instruction. Processors with indirect forms of subroutine call instructions lend themselves to this technique directly. In assembly language programming this constitutes a powerful technique for rapidly and efficiently altering program flow. Some high-level languages, such as Modula-2 and C, also have the ability to utilize indirect calls. This article illustrates the use of the standard types PROCEDURE and PROC in Modula-2. The examples contained in this article were compiled and testing using the Fitted Software Tools Version 2.0 Modula-2 compiler system, but I have tried to make the code as generic as possible. They should compile and run with other compiler types with no modifications, since PROC is part of the original language definition according to Niklaus Wirth. Dynamic Linked Lists Versus CASE Structures Before we get started, perhaps a comment about linked lists is in order. Yes, there is a way to perform similar functions in Modula-2 by using dynamic linked lists, each node of which contains a pointer to particular procedure. I have chosen not to delve into this area, however, because it is already well covered in most standard texts on the language. Niklaus Wirth gives an excellent example of this technique in his book, Programming in Modula-2, and I would refer the reader there for more information. Although much has been written on the use of the Modula-2 procedure type in linked list data structures, references to its application in building vector-table type constructs appear to be rather scarce. My main focus here is the design and construction of application dependant code based on the use of the CASE structure. In this article we will examine this alternative usage of the procedure type in conjunction with the creation of a complex user interface based on layered menus. This type of application often relies heavily on multiple CASE structures to evaluate user selections. The use of the procedure types allows the programmer to eliminate redundant CASE structures in the code and replace them with a single procedure vector dispatch handler. The Procedure Type In Modula-2 In Modula-2, the procedure type may be declared in one of two ways: (1) as a parameter-defined type, or (2) as a parameterless type. The term "parameter-defined" is used to mean those occurrences of the procedure type where one specifically defines what parameter types will be used. An example of this would be: VAR SomeProcedure : PROCEDURE(CARDINAL,CARDINAL); Then, later in the code, SomeProcedure := WriteCard; may be used to assign WriteCard from the standard library to the name SomeProcedure. By the same token, any procedure that accepts two parameters of type CARDINAL may be assigned to SomeProcedure. The second form is somewhat more generic, and is coded as: VAR SomeProcedure : PROC; When this is used one does not need to declare parameters, but the parameters of the procedure pointed to by the variable designated as being of type PROC must match the actual parameters passed to it. Using The PROC Type - Examples Consider a typical problem: You are writing a rather large applications package that makes extensive use of menu screens and associated CASE structures to route the user to various portions of the code. If you took a standard approach, you would have to duplicate the code for the menu display and CASE routing of the user's selection for each and every menu in the system. For a program of even modest size this may begin to approach twenty or so menus, each with its own section of CASE code. That's a lot of wasted code, especially for those paths in the menu tree that are seldom traversed. It would seem to make more sense to define a list of possible target procedures, and then hand it to one procedure that does nothing but select procedure calls based on input from the user. This, then, would reduce the problem to simply constructing vector-tables for each of the possible menu displays. The CASE vector structure would never change, and could be reused any number of times. The key to this approach is to use the type PROC to build a one- dimensional array. The program show in Listing 1 illustrates the use of an array of type PROC. Notice that the procedures called have no parameters. An important point to notice here is the way that the execution loop is tested for a valid end condition. Instead of attempting to determine which procedure is currently executing, the relative pointer value is used. This is because there exists no type coercion mechanism between an array of type CHAR (a string) and the type PROC. While the previous example may be interesting, and in some cases useful, the next example contained in Listing 2 shows where this technique really shines. Here we have basically the same program, only now a crude user interface has been added. When the user selects a number from the menu, the appropriate procedure is called from the vector-table. Also notice that the execution loop is now itself a separate procedure. The main body of the program simply passes an array of procedures to use as the vector-table and the relative vector pointer into the array. One might ask at this point: "Why have a separate procedure to handle the vector dispatch?". There is a good reason for this. Because Modula-2 allows the programmer to easily pass arrays as procedure parameters, separating the vector-table dispatcher from the rest of the code allows one to give it any pre-defined array of procedure pointers. In other words, it becomes a generic, and hence reusable, procedure. Handling Variable Size Vector Selection Lists The reader may notice that this implementation of the technique does have a fundamental limitation: The case structure in the vector dispatch handler should have less or the same amount of choices as the vector-table array has declared elements. This lack of generalization also precludes the technique from implementation as part of a library module, unless one is willing to define very large vector-table arrays to handle most conceivable cases. One way to resolve the array indices problem mentioned above would be to define both the vector-table array and the vector handler CASE structure for the maximum possible number of choices in a particular program application. If one inspects the declaration portion of both example programs it will be seen that this has, in fact, been done for the vector-table array. In Listing 2, also notice that the procedure Jump has the ability to utilize the entire vector-table array, but that the table itself only contains five valid vectors. One method for trapping an "early-end" condition is illustrated in the way that menu item 6 is trapped by an IF-THEN-ELSE structure in the main body of Listing 2. The vector selection list for any given menu is simply overlaid on the vector-table array, and the trap point set for the end of the list + 1. Towards Eliminating Redundant Code Finally, there is one further trick in this rather interesting bag. The main body of Listing 2 could itself be made into a sub-procedure that accepts the name of a menu display array (ARRAY [0..n] OF StringType), and a vector list. This will eliminate most of the redundant menu handling code, since the programmer would now need only to define the following items for each menu in the system: 1 - The Menu Display 2 - The Vector Selection List 3 - The Menu Item Cut-Off Point (list item + 1) The header line for this procedure might look something like this: DoMenu ( Menu : MenuDisplay; Vlist : ProcList); Alternatively, one could define a record structure with all the necessary elements for each menu: TYPE VDef = RECORD MenuLine : ARRAY [0..80] OF CHAR; PVector : PROC; END; VAR MenuDef : ARRAY [0..n] OF VDef; where n is the number of selection items in the menu. Acknowledgements I would like to express my appreciation to Roger Carvalho, the author of the FST compiler, for the time he spent reviewing this article and making helpful comments and corrections. Product Reference Fitted Software Tools P.O. Box 867403 Plano, Texas 75086 FST Modula-2 Compiler System, Version 2.0 ------------------------------------------------------------------- Listing 1 - Demonstration of type PROC as an array MODULE JmpTest1; FROM InOut IMPORT WriteString,WriteLn; TYPE ProcList = ARRAY [1..9] OF PROC; VAR VectorTo : ProcList; (* Procedure vector table *) ProcNum : CARDINAL; (* Relative vector pointer *) (* ************ Test Procedures *************************** *) PROCEDURE Proc1; BEGIN WriteString("Number 1 worked....");WriteLn; END Proc1; PROCEDURE Proc2; BEGIN WriteString("Number 2 worked....");WriteLn; END Proc2; PROCEDURE Proc3; BEGIN WriteString("Number 3 worked....");WriteLn; END Proc3; PROCEDURE Proc4; BEGIN WriteString("Number 4 worked....");WriteLn; END Proc4; PROCEDURE Proc5; BEGIN WriteString("Number 5 worked....");WriteLn; END Proc5; (* ************************ Main Body *************************** *) BEGIN VectorTo[1] := Proc1; (* Load the call table *) VectorTo[2] := Proc2; VectorTo[3] := Proc3; VectorTo[4] := Proc4; VectorTo[5] := Proc5; ProcNum := 1; (* init the table pointer *) (* call each proc in table until the end is reached *) WHILE ProcNum < 6 DO VectorTo[ProcNum]; INC(ProcNum); END; WriteLn; END JmpTest1. (* ************************************************************** *) ------------------------------------------------------------------- Listing 2 - User Interface Example MODULE JmpTest2; FROM InOut IMPORT WriteString,WriteLn,ReadCard; TYPE ProcList = ARRAY [1..9] OF PROC; VAR CallList : ProcList; ProcNum : CARDINAL; ExitLoop : BOOLEAN; UserIn : CARDINAL; (* ************ Test Procedures *************************** *) PROCEDURE Number1; BEGIN WriteLn; WriteString("Number 1 worked....");WriteLn; END Number1; PROCEDURE Number2; BEGIN WriteLn; WriteString("Number 2 worked....");WriteLn; END Number2; PROCEDURE Number3; BEGIN WriteLn; WriteString("Number 3 worked....");WriteLn; END Number3; PROCEDURE Number4; BEGIN WriteLn; WriteString("Number 4 worked....");WriteLn; END Number4; PROCEDURE Number5; BEGIN WriteLn; WriteString("Number 5 worked....");WriteLn; END Number5; (* ******************* Vector-Table Handler ********************* *) PROCEDURE Jump ( JMPptr : CARDINAL; JMPList : ProcList); BEGIN CASE JMPptr OF 1 : JMPList[1]; | 2 : JMPList[2]; | 3 : JMPList[3]; | 4 : JMPList[4]; | 5 : JMPList[5]; | 6 : JMPList[6]; | 7 : JMPList[7]; | 8 : JMPList[8]; | 9 : JMPList[9]; ELSE WriteString("Invalid Vector Pointer");WriteLn; END; END Jump; (* ************************ Main Body *************************** *) BEGIN CallList[1] := Number1; (* Load the call table *) CallList[2] := Number2; CallList[3] := Number3; CallList[4] := Number4; CallList[5] := Number5; ExitLoop := FALSE; (* loop control switch *) (* This loop will repeat the menu display until the user signals that an exit is desired. *) WHILE NOT ExitLoop DO WriteLn; WriteLn; WriteString(" 1 - Menu Item Number One");WriteLn; WriteString(" 2 - Menu Item Number Two");WriteLn; WriteString(" 3 - Menu Item Number Three");WriteLn; WriteString(" 4 - Menu Item Number Four");WriteLn; WriteString(" 5 - Menu Item Number Five");WriteLn; WriteLn; WriteString(" 6 - Exit");WriteLn; WriteLn; WriteString(" Your Selection? -> "); ReadCard(UserIn); WriteLn; IF UserIn < 6 THEN (* test for menu exit *) Jump(UserIn,CallList) ELSE ExitLoop := TRUE END; (* IF *) END; (* WHILE *) END JmpTest2. (* ************************************************************** *) -- | John M. Hughes | "...unfolding in consciousness at the | | noao!jmh%moondog@coyote | deliberate speed of pondering." - Daniel Dennet | | jmh%coyote@noao.edu |--------------------------------------------------| | noao!coyote!jmh | P.O. Box 43305 Tucson, AZ 85733 |