[comp.lang.modula2] The PROC type

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                |