10.7 Library Modules--Scope and Visibility Rules

Consider for a moment the modules already used, and recall that the WriteString of STextIO is not the same procedure as the WriteString of TextIO. A number of previous examples have utilized the fact that when both are needed, a clash can be avoided by writing:


among the other IMPORT lists and then referring to the qualified identifier STextIO.WriteString when it was required instead of the WriteString from TextIO.

The astute reader should realize that this treatment must be a consequence of the very nature of library modules, for since their exported items are available to be imported into unknown scopes, all those exports must in fact be QUALIFIED. Not only is this true, but in earlier versions of Modula-2, it was also necessary to include the line

EXPORT QUALIFIED <list of entities>;

in every definition module, so as to specify which items were available for import by program modules. However, since the normal reason for not exporting an item was to have it for the private use of the implementation, Wirth decided that the meaning of a definition module should include an implicit qualified export of all items listed in it, so this line is no longer necessary.

Thus, there is always a choice between importing only the library name and having to refer to all its items qualified, or listing the items in an unqualifying FROM and then referring to them unqualified.

NOTE: Some (including many of those involved in the ISO work) prefer to import always by library name only, never by item name. They use all library items in qualified fashion at all module levels. On the other hand, the author of this text takes the view that in most cases, qualified identifiers are too cumbersome for code writing, and avoids them unless there is some other reason than the additional clarity alleged for individual statements.

10.7.1 Access to Imported Libraries at Inner Scopes

As already observed, IMPORT and EXPORT, of whatever kind, can only affect visibility one scope at a time. Thus, when one writes:


Fries becomes visible in the scope where the IMPORT is being done, provided French was already visible in that scope. This in turn implies that one could have referred to French.Fries in the scope where French is visible without doing any IMPORT at all, unless, of course, the module French is visible only because it is actually resident on the disk, and is only visible in the global scope.

The first part of this chapter included a discussion of global and local identifiers, and made the observation that, since a program executes as a procedure, an identifier declared at the outermost level is really local to the outer block rather than global, though the difference in practice is not important.

Now, because modules are really identifiers used in a way that allows control over visibility, the same considerations of global versus local apply to them as to any other identifier. That is, a module can be declared at or imported into the outermost level of a program (global) or declared at or imported into some module buried inside the program (local - to some degree).

Specifically, if a local module requires some entities from external libraries, they must first be made available in the main program module, after which they can be imported to some inner scope. Here is a follow-up from the last example to illustrate this. This one is like LocalDemo, except that both procedures are inside the local module. Since imports can only come from the surrounding scope, note how the local module gains the use of library modules, with either an import of items directly visible there, or of ones in libraries visible there.

MODULE LibVisibilityDemo;

IMPORT WriteString;
  WriteString ("I can do this here.");
END Inner;

(* WriteLn visible only as STextIO.WriteLn *)

END LibVisibilityDemo.

Note that if the program module requires entities from STextIO, it can import them unqualified as well as qualified (FROM). Since only one copy of an item obtained from an imported module lives in the computer's memory at any one time, no additional storage space is required to do both types of imports.

Any items not directly needed in the outer scope ought to be imported only as part of whole libraries, then imported into inner scopes as needed. Then, only those items actually being used are available in each scope.

10.7.2 Visibility in Library Modules

Like any other modules, library modules may employ only those entities that they either define themselves, or that they import from elsewhere. These imports are subject to some special rules that apply only to library modules. These were referred to in Chapter 6, and are included here for the sake of completeness.

1. Any entities declared in the DEFINITION part of the library module are automatically visible to the IMPLEMENTATION part of the module.
2. The converse is false. Items declared only in the implementation part are visible only in the implementation part.
3. Items imported from elsewhere by the definition part (for instance, types for parameter lists) are available only in the definition part. They must be imported separately by the implementation part.
4. Even if the definition part redefines an enumerated import as its own, that does not make the field names of the original item visible in the implementation.

Thus, one has, for instance:


IMPORT IOChan, ChanConsts, SYSTEM;
  ChanId = IOChan.ChanId;
  FlagSet = ChanConsts.FlagSet;
  OpenResults = ChanConsts.OpenResults;
  (* Accepted singleton values of FlagSet *)
  read = FlagSet{ChanConsts.readFlag};
  write = FlagSet{ChanConsts.writeFlag}; 
  old = FlagSet{ChanConsts.oldFlag};
  text = FlagSet{ChanConsts.textFlag};
  raw = FlagSet{ChanConsts.rawFlag};

PROCEDURE OpenOld (VAR cid: ChanId; name: ARRAY OF CHAR; flags: FlagSet; VAR res: OpenResults);

(* more stuff *)

END RndFile.

where assorted items are imported for the use of the definition module, and a number of them are redefined for the convenience of other modules that might want to import these common items from here, the redefined items are all available in the implementation part, but it must import its own copy of SYSTEM, and it does not have access to the enumeration values of the re-defined OpenResults directly; it must refer to them by importing ChanConsts.OpenResults itself and employing qualified identifiers of that module. Here is a portion of the author's implementation:

IMPORT ChanConsts, IOConsts, IOLink, IOChan, SYSTEM;
(* all the rest here *)

END RndFile.

10.7.3 Opaque Types--a Brief Introduction

The more curious readers may have peeked into the Appendices by now and looked over some of the definition modules reproduced there. If so, they may have noticed a few cases where there was a line like:


with no actual definition of what the type was. In this case, the details of the type definition are hidden away in the implementation module, and are not accessible to the programmer. One reason this is done is so that the same definition module can serve many different computers, even though something like a ChanId might have a completely different structure, depending on the operating system or the disk format.

A type that is exported from a definition module, but whose details are hidden in the implementation module is called an opaque type. Types whose details are visible (normal types) are called transparent types.

These last two definitions are presented for information at this point. This book will not be making use of opaque types for a while yet. Suffice it to say for now that opaque types allow hiding the details of data from the view of the main program and prevent it from becoming corrupted inadvertently. All access to that data is controlled by the procedures written specifically for that purpose.