modula-2 home

  Home  
  Tutorial  
  Win32 API  
  Reference  
  Projects  
 

 

Chapter 12 - Modules, Local and Global


Prerequisites for this material

Before attempting to understand this material, you should have a good grasp of the principles taught in Part I of this tutorial. None of the material from Part II is required to do a meaningful study of modules in Modula-2.

What good are modules?

Modules are the most important feature of Modula-2 over its predecessor Pascal making it very important for you to understand what they are and how they work. Fortunately for you, there are not too many things to learn about them and after you master them you will find many uses for them as you develop programs, and especially large programs.

Load and display the program named LOCMOD1.MOD for your first example of a program with an embedded module. Modules are nothing new to you because every program you have examined has been a module. At this time, however, we will introduce a local module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
MODULE LocMod1;

FROM Terminal2 IMPORT WriteString, WriteCard, WriteLn;

VAR Index : CARDINAL;

       MODULE LocalStuff;
       EXPORT GetNumber;    (* Nothing else is visible outside *)
                            (* Nothing outside is visible here *)
       VAR Counter : CARDINAL;

              PROCEDURE GetNumber() : CARDINAL;
              BEGIN
                 Counter := Counter + 3;
                 RETURN Counter;
              END GetNumber;

       BEGIN
       Counter := 4;    (* This is only run at load time *)
       END LocalStuff;

BEGIN      (* Main program *)
   FOR Index := 1 TO 8 DO
      WriteString("The count is now ");
      WriteCard(GetNumber(),8);
      WriteLn;
   END;    (* Do loop *)
END LocMod1.

What is a local module?

A local module is simply a module nested within another module, just like the example on your monitor at this time. The module named "LocalStuff" is nested within the main module and is heavily indented for clarity. Since nothing is imported into the local module, nothing that belongs to the main module can be seen from within the nested module. In addition, since the procedure "GetNumber" is the only thing exported from the local module, nothing else is available to the main module. In effect, the local module is an impenetrable wall through which nothing can pass without the benefit of the IMPORT and EXPORT list. In this case, the variable "Counter" cannot be modified in any way by the main module, either intentionally or accidentally and the procedure "GetNumber" will very stubbornly refuse to allow any flexibility in its output, adding three to its internally stored variable each time it is called. It may seem to you that this result can be accomplished easily by using another procedure without the module but we will see shortly that it will not be the same.

The body of the local module

The body of the local module has one statement contained within it, "Counter := 4;", that is executed only when the module is loaded, and at no other time. This is therefore an initialization section for the module. Any valid statements can be put here and they will be executed when the program is loaded, or you can omit the body altogether by omitting the BEGIN and any statements. Actually, this body is no different than the body of the main program since it too is executed one time when the program is loaded, except for the fact that the main program is required to have a body or you will have no program.

The module versus the procedure

We must digress a bit to see the difference in these two important topics in Modula-2. A procedure is an executable section of code whereas a module is a grouping of variables, constants, types, and procedures. A module is never executed since it is simply a grouping identifier.

The variables in a procedure do not exist when it is not being executed, but instead are generated dynamically when the procedure is called. A variable therefore, has a lifetime associated with it in addition to a "type". This may seem strange to you but if you think about it for awhile, it will help explain how recursive procedure calls work. The module, on the other hand, exists anytime its surrounding code exists, in this case, the main program. Since the module always exists, the variable "Counter" also always exists because it is defined as a part of the module. If this variable were defined within a procedure, it would be automatically regenerated every time the procedure were called and would therefore not remember the value it contained the prior time the procedure was called. We could choose to define the variable as global and it would therefore always be available and never regenerated, but we would be left with the possibility of anything in the program modifying it either accidentally or on purpose. In a program as small as this one, it would not be a problem, but it is intended to illustrate the solution to a problem embedded in a much larger program.

Suppose, for example, that you wished to generate random numbers for some use within a program. You could include all of the code within a module using the module body for the seed initialization, and a procedure to generate one random number each time it was called. The structure would be essentially the same as that given here, but the actual code would be different. Nothing in the main program or any of its procedures could in any way corrupt the job given to the random number generator.

Back to the program on your monitor

In this case we have one local module defined within the main module but as many as desired could be used, and we have one procedure in the local module whereas we could have as many as desired. In fact, we could have local modules embedded in a procedure, or in other local modules. There is no real limit as to how you can structure your program to achieve the desired results. One thing must be remembered. If you embed a local module within a procedure, all of its variables are defined dynamically each time the procedure in which it is embedded is called, and its body is also executed each time. This can be used to advantage in some situations, but it would be best to leave this construct to the future when you have more experience with Modula-2.

In the body of the main module you will find nothing new except for the call to the function procedure "GetNumber()" which is actually nothing new except that it is embedded in the local module "LocalStuff". Compile and run the program to see if it does what you expect it to do.

Two local modules

It would be well to point out at this time that if you define two local modules at the same level, one could EXPORT a variable, procedure, constant, or type and the other could IMPORT it and use it in any legal fashion. You therefore have the ability to very carefully define the mechanism by which the two modules interact.

Another local module

The program we have been inspecting had the procedure exported without qualification, so it could only be referred to by its simple name. This could have led to a naming conflict which can be solved by using a qualified export as is done in the next program. Load and display the program named LOCMOD2.MOD. This program is very similar to the last except for moving the output statements to the procedure.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
MODULE LocMod2;

FROM Terminal2 IMPORT WriteString, WriteCard, WriteLn;

VAR Index : CARDINAL;

     MODULE MyStuff;
     IMPORT WriteString, WriteCard, WriteLn;
     EXPORT QUALIFIED WriteStuff;
     VAR Counter : CARDINAL;
          PROCEDURE WriteStuff;
          BEGIN
             Counter := Counter + 3;
             WriteString("The value of the counter is ");
             WriteCard(Counter,8);
             WriteLn;
          END WriteStuff;
     BEGIN
        Counter := 4;
     END MyStuff;

BEGIN      (* Main program *)
   FOR Index := 1 TO 8 DO
      MyStuff.WriteStuff;
   END;
END LocMod2.

First, you should notice that the procedure name is exported using "EXPORT QUALIFIED" which allows the use of the qualified call to the procedure in line number 25. There can never be a conflict of names in calling a procedure this way because it is illegal to use the same name for a module more than once at any level. In a local module, you have a choice of using either qualified or unqualified export of items, but the exported items must all be of the same export type because only one export list is allowed per module.

Importing into a local module

The three output procedures are used in the local module "MyStuff", but because it is only permissible to import items from a module's immediate surroundings, the procedures must first be imported into the main module.

The procedure named "WriteStuff" is even more tightly controlled than that in the last program because this one doesn't even return a value to the calling program. It updates its own internally stored value, displays it, and returns control to the calling program.

Compile and run this program, then we will go on to global modules.

Global modules

As useful as local modules are, they must take a back seat to the global module with which you are already fairly familiar because you have been using them throughout this tutorial. The modules "InOut", "Terminal", and "FileSystem" are examples of global modules that you already know how to use. Now you will learn how to write your own global modules that can be called in exactly the same way as these standard modules from any program.

Your first definition module

In order to get started, load and display the program named CIRCLES.DEF on your monitor. The first thing you will notice is that we used a different extension for this program because there is another part to the program with the same name but the usual extension "MOD". What you have displayed on your screen is the definition part of the global module and it serves two very important purposes. First, it defines the interface you need to use the module in one of your programs, and it defines the details of the interface for the compiler so it can do type checking for you when you call this module. The Modula-2 compiler uses the information contained here to check all types and numbers of variables just like it would do in a singly compiled program.

1
2
3
4
5
6
7
DEFINITION MODULE Circles;

PROCEDURE AreaOfCircle(Radius : REAL; VAR Area : REAL);

PROCEDURE PerimeterOfCircle(Radius : REAL; VAR Perim : REAL);

END Circles.

The program on your monitor does very little. In fact its purpose is to do nothing because there are no executable statements in it. It is only to define the interface to the actual program statements contained elsewhere. Notice that the procedures are exported using the qualified option. All identifiers that are exported from a definition module must be qualified so that the user has the option of importing them either way. It is legal to export procedures, variables, constants, or types for use elsewhere as needed for the programming problem at hand, but the majority of exported items are procedures. It should be obvious that nothing within the module is available to any other part of the program unless it is exported.

The implementation module

We are not finished with the definition part of the module yet but we will look at the implementation part of it for a few moments. Load the program named CIRCLES.MOD and display it on your monitor. This is the part of the module that actually does the work. Notice that there are three procedures here, two of which were defined in the definition part of the module making them available to other programs. The procedure named "GetPi" is a hidden or private procedure that is only available for use within this module. The other two procedures are available to any program that wishes to use them simply by importing them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
IMPLEMENTATION MODULE Circles;

PROCEDURE GetPi(VAR Pi : REAL);
BEGIN
   Pi := 3.14159;
END GetPi;

PROCEDURE AreaOfCircle(Radius : REAL; VAR Area : REAL);
VAR Pie : REAL;
BEGIN
   GetPi(Pie);
   Area := Pie * Radius * Radius;
END AreaOfCircle;

PROCEDURE PerimeterOfCircle(Radius : REAL; VAR Perim : REAL);
VAR Cake : REAL;
BEGIN
   GetPi(Cake);
   Perim := 2.0 * Cake * Radius;
END PerimeterOfCircle;

BEGIN (* IMPLEMENTATION MODULE body, empty in this case *)
END Circles.

Anything defined in the definition part of the module is also available here for use without redefining it, except for the procedure headers which must be completely defined in both places. Anything imported into the definition part of the module must also be imported here if it will be used in this module, imported identifiers are not automatically transferred into this part of the module.

More about the use of two parts

The definition part of the module defines the public information about the module and the implementation part of the module defines the private or hidden information about the module. It may seem sort of silly to go to the trouble of separating a module into two parts but there are at least three good reasons to do so.

1. You may not care how the module is implemented.

In all of the programs we have run up to this point, you probably didn't care how the "WriteString" procedure did its job. You only wanted it to do the job it was supposed to do to aid you in learning to use Modula-2 efficiently. It would have been senseless to have cluttered your monitor with the details of how it worked every time you wanted to know how to use it.

2. It hides details of implementation.

If you were working on a large programming project and you were assigned to job of writing a procedure for others to use that did some well defined task, you would define the interface carefully and be finished. If, however, one of the users studied your detailed code and found a way to trick it into doing something special, he may use the trick in his part of the program. If you then wanted to improve your routine and remove the code that allowed the trick, the interface would no longer work. To prevent this, you give others only the interface to work with and they cannot look for tricks. This is called "information hiding" and is a very important technique which is used on large projects.

3. It allows for orderly development.

It is possible to define all of the definition parts of the modules and have all members of the development team agree to the interface. Long before the details of the individual procedures are worked out, the entire team knows what each procedure will do and they can all begin work on their respective parts of the overall system. This is very effective when used on a large team effort.

Compilation order is important

In order for the above principles to work effectively, a very definite order of compilation must be adhered to. If the identifiers declared in the definition part are automatically available in the implementation part of the module, then it is obvious that the definition part must be compiled before the implementation part of the module can be compiled. Also, if the definition part is modified and recompiled, then the implementation part may also require modifications to comply with the changes and it must also be recompiled.

The next rule is not nearly so obvious but you will understand it when we explain it. When a calling module is compiled, it checks each of the imported identifiers to see that the types and number of variables agree with the calling sequences used in the program. This is part of the strong "type checking" done for you by Modula-2. If you modify and recompile one of the called definition modules and attempt to relink the program together, you may have introduced a type incompatibility. In order to prevent this, Modula-2 requires you to recompile every module that calls a modified definition module. It does this by generating a "key" when you compile a definition module and storing the "key" when you compile the calling module. If you attempt to link a program with differing "keys", this indicates that the definition module was changed, resulting in a new "key" and hence a mismatch, and the linker will generate an error.

Why all of this trouble?

It may not seem to be worth all of the extra trouble that the Modula-2 compiler and linker go through to do this checking but it is important for a large program. The information used in the definition part of the module is the type of information that should be well defined in the design stages of a programming project, and if well done, very few or no changes should be required during the coding phase of the project. Therefore it is expected that recompiling several definition modules should not happen very often. On the other hand, during the coding and debugging phase of the project, it is expected that many changes will be required in the implementation parts of the modules. Modula-2 allows this and still maintains very strong type checking across module boundaries to aid in detecting sometimes very subtle coding errors.

The above paragraph should be interpreted as a warning to you. If you find that you are constantly recompiling modules due to changes in the definition modules, you should have spent more time in the software design.

Now to actually use it all

With all of that in mind, it will be necessary for you to reload the program named CIRCLES.DEF which is the definition part of the module, and compile it. Your compiler will generate several different files for use in cross checking. After you get a good compile, reload the program named CIRCLES.MOD which is the implementation part of the module and compile it. During this compile, some of the files generated by CIRCLES.DEF will be referred to. It would be an interesting exercise to modify a procedure call in one of the programs to see what kind of an error is displayed. After a good compile on both of these modules, you have a new module in your library that can be used just like any of the other global libraries that came with your compiler.

Load and display the program GARDEN.MOD for an example of a program that calls your new library or global module. This program is very simple and should pose no problem in understanding for you. The two new procedures are imported and used just like any other procedure. Compile and run this program.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
MODULE Garden;

FROM Terminal2 IMPORT WriteString, WriteLn, WriteReal;
FROM Circles   IMPORT AreaOfCircle, PerimeterOfCircle;

VAR Radius, Area, Circumference : REAL;

BEGIN    (* Main program *)
   Radius := 12.0;
   AreaOfCircle(Radius,Area);
   PerimeterOfCircle(Radius,Circumference);

          (* Calculations complete - output results *)

   WriteString("Radius = ");
   WriteReal(Radius,12);
   WriteString("     Area =");
   WriteReal(Area,12);
   WriteLn;
   WriteString("Circumference = ");
   WriteReal(Circumference,12);
   WriteLn;
END Garden.

A final word about global modules

From the above description of global modules, it may not be very obvious to you that it is perfectly legal for one global module to call another which in turn calls another, etc. Program structure is entirely up to you. For example, we could have called "WriteString" and some of our other familiar procedures from within the "AreaOfCircle" procedure. The order of compilation must be kept in mind or you will not get a good compilation and linking of your completed program.

Remember that there is nothing magic about the global or library (the names are synonymous) modules supplied with your compiler. They are simply global modules that have already been programmed and debugged for you by the compiler writer. This is probably a good time to mention to you that you may have only received the source code for the definition part of the library modules with your compiler. Many compiler writers will supply the source code for the implementation part of the library modules only if you supply them with a little more money. After all, they are in business for the money and most people never wish to modify the supplied routines but are happy to use them as is. All compiler writers will supply you with the definition part of the library modules because they are your only means of interfacing with them.

The PROCEDURE type, something new

Load and display the program named PROCTYPE.MOD on your monitor for an example of a procedure TYPE. In line 6 we define a variable "OutputStuff" to be a PROCEDURE type of variable that requires an "ARRAY OF CHAR" as an argument. This variable name can now be used to refer to any procedure that uses a single "ARRAY OF CHAR" as an argument.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
MODULE ProcType;

FROM Terminal2 IMPORT WriteString, WriteLn;

VAR OutputStuff : PROCEDURE(ARRAY OF CHAR);
    VarLine     : ARRAY[0..50] OF CHAR;

     PROCEDURE WriteWithNote(InString : ARRAY OF CHAR);
     BEGIN
        WriteString("Written with a note ---> ");
        WriteString(InString);
        WriteLn;
     END WriteWithNote;

     PROCEDURE WriteWithComment(InputLine : ARRAY OF CHAR);
     BEGIN
        WriteString(InputLine);
        WriteString(" <--- Written with a comment");
        WriteLn;
     END WriteWithComment;

BEGIN
   VarLine := "This is a line of data.";
                                        (* This uses WriteWithNote *)
   OutputStuff := WriteWithNote;
   OutputStuff(VarLine);
   OutputStuff("Extra output ");
                                      (* This uses WiteWithComment *)
   OutputStuff := WriteWithComment;
   OutputStuff(VarLine);
                                          (* This uses WriteString *)
   OutputStuff := WriteString;
   OutputStuff(VarLine);
   OutputStuff(" End of the line");
   WriteLn;
               (* The Procedures can be used in normal fashion too *)
   WriteLn;
   WriteWithNote("This is straight output.");
   WriteWithComment("This too is straight output.");
   WriteString(VarLine);
END ProcType.

In the definition part of the program two procedures are defined, each of which uses a single "ARRAY OF CHAR" as an argument. Then in the main program the variable "OutputStuff" is assigned each of the new procedures and used to call them. In addition, it is used to call the supplied procedure "WriteString" to illustrate the possibility of doing so. Finally, the procedures are all called in their normal manner to illustrate that there is nothing magic about them. Any procedure type can be used to call any procedures that use the same type of parameter calls as those defined when it is created as a variable.


 

Next: Chapter 13. Machine Dependent Facilities