19.3 Getting Started with Object Oriented Modula-2

19.3.1 A Little History

Classical Modula-2 as originally defined by Wirth did not have any object notation whatsoever. Later, Wirth himself devised some simple object extensions for Pascal, and this Object Oriented Pascal became the language in which the entire Macintosh operating system was written. Because of the surface similarities between Pascal and Modula-2, some vendors adopted these extensions for their versions of Modula-2 as well.

When the ISO committee produced the initial base standard (ISO/IEC 10514-1) for the Modula-2 notation, it also lacked any object notation. However, by the time this standard was published WG13 (modula-2) was already developing the supplementary standards for Generic Modula-2 (ISO/IEC 10514-2) and Object Oriented Modula-2 (ISO/IEC 10514-3), and these were subsequently approved as supplementary and independent standards. That is, vendors could implement only the base standard if they wished, or they could include in their package either or both of the additional parts. Most are expected to include both.

In the light of the introductory discussions, it is worthwhile noting some of the decisions the committee took with respect to Object Oriented Modula-2 (often abbreviated to OOM-2).

There are several other issues on which the committee had to make decisions, but these will be pointed out when the related concepts are introduced later in the chapter.

19.3.2 Some Simple OOM-2 Programs

Example 1:
The purpose of this example is to illustrate some of the simple syntax for declaring and using OOM-2 classes and objects. The module establishes a class of objects called rectangle with some attribute and method components, and generates two messages to those components. Its only output is to print the area before (0) and after (12) the call to SetDims.

The keyword CLASS is used to declare a class, in like manner to the use of TYPE in the base language. Observe that the keyword REVEAL is used to allow visibility outside the class, and that the class maintains the hidden variables length and width internally. In Modula-2 classes are untraced unless marked with TRACED, and modules are safeguarded unless marked with UNSAFEGUARDED. Note the initialization code, which is automatically applied whenever an object of this class is instantiated with the OOM-2 pervasive CREATE. It is important to remember that the declaration of entities of the class rectangle in a VAR clause only sets aside enough static memory for the reference variable; no dynamic memory is allocated until the object is instantiated with the CREATE statement.

MODULE TestRectangleClass1;
(* to demonstrate simple traced class syntax
  by R. Sutcliffe 1998 09 24 *)


TRACED CLASS rectangle;
  REVEAL SetDims, Area, sides;
  (* declare components of class *)
  (* first the attribute components *)
    sides = 4;
   length, width : INTEGER;
  (* and then the method components *)
  PROCEDURE SetDims (len, wid : INTEGER);
    length := len;
    width := wid;
  END SetDims;
    RETURN length * width;
  END Area;
BEGIN (* initialization *)
  SetDims (0,0);
END rectangle;

  theRect : rectangle;

BEGIN (* main *)
  CREATE (theRect);
  (* print out initial area *)
  SWholeIO.WriteInt (theRect.Area(), 10);
  theRect.SetDims (4, 3);
  (* print out area after new dimensions set *)
  SWholeIO.WriteInt (theRect.Area(), 10);
END TestRectangleClass1.

Observe the prettyprinting style. Here, the components of the class (but not its body) have been indented for easy reading, and the first line has been treated somewhat like a procedure heading by putting it all on one line, rather than as a type heading and starting a new line. This is because TYPE introduces a whole section that could include several declarations, whereas CLASS heads only one such declaration at a time.

Example 2:
Here is an implementation in OOM-2 of the declaration of the Account class discussed in earlier sections. No code has been written to declare and instantiate individual accounts at this time.

MODULE TestAccount1;
(* to demonstrate simple traced class syntax
  by R. Sutcliffe 1998 09 24 *)

  REVEAL Credit, Debit, READONLY balance;
    balance : REAL;
    overdrawn : BOOLEAN;
  PROCEDURE Credit (amount : REAL);
    balance := balance + amount;
  END Credit;
  PROCEDURE Debit (amount : REAL);
    balance := balance - amount;
  END Debit;
  PROCEDURE CheckOverdraw;
    IF balance < 0.0 
        overdrawn := TRUE
        overdrawn := FALSE;
  END CheckOverdraw;

BEGIN (* initialization *)
  balance := 0.0;
  overdrawn := FALSE;
END Account;

END TestAccount1.

Any number of accounts can be declared in a VAR clause and then instantiated by a CREATE statement. Each one has its own copies of the attribute components, so that when any of these is used in a method component or is referred to by a client, the data obtained is that belonging to the object that invoked the member. The reader should observe that this is a major difference between classes and modules, for any data global to an ADT declared in a module is global to all entities of the type that are declared, not associated with each one individually.

19.3.3 Summary of Basic OOM-2 Traced Class Semantics

Both examples in the previous section were of traced classes. The reserved word TRACED must precede any such class to indicate to the garbage collector that it is to trace objects of this class from the time they are instantiated and, if all references to the object cease to exist, it may destroy the object by reclaiming its memory.

A traced class declaration may contain constant, type, variable, and method (procedure) declarations. It also defines a scope of visibility for these items, and unless one or more of them is revealed outside that scope, they are not visible in the surrounding module. Thus, REVEAL serves a purpose in the context of a class within a module similar to that served by EXPORT in the context of a module within a module.

On the other hand, Modula-2 class declarations clearly have some similarity to records. The components of a class are in a sense similar to the fields of a record.

Constants and types declared in a class declaration can be referred to by using the appropriate identifier qualified by the class name in the surrounding scope, provided they have been revealed there. Of course, constants can also be referred to qualified by any object name instantiated from that class. However, the base rules of the language prohibit use of a type name qualified by a variable name, so such references are not useful. Moreover, OOM-2 states that the reference to a constant qualified by an object name is not a constant expression, so it cannot be used in such situations.

However, variables declared in a class differ from variables declared in a module in that there is no memory associated with them until an instance of the class has been created. The memory is associated with each individual instance or member, not with the class as a whole, and it is reserved by a call to CREATE. They cannot be referred to qualified by the class name, only qualified by an object instance name of that class. This memory also cannot be manually returned by a corresponding destructor; it is completely under the control of the garbage collector thereafter.

Moreover, although methods are declared with the reserved word PROCEDURE, and they do look and behave very much like procedures, they are useful only with members of the class in which they are declared. Unlike procedures declared elsewhere in a module, they do not have a type; therefore, there are no method variables or method constants like there are procedure variables and constants, and method names also cannot be referred to qualified by the class name, only by an object name.

Not only do Modula-2 object references get their memory when CREATE is called, the data fields of the object can be initialized automatically at that time by placing statements in the body of the class declaration.

Should the programmer decide to re-use a traced object reference, say, in a loop, by calling CREATE on an object reference a second time, a new memory allocation is done, just as when doing this with NEW on a pointer. Thus,

CREATE (thingy);
 (* some code here *)
CREATE (thingy); (* again *)

results in a different reference for thingy. As with pointers, unless the previous value has been stored elsewhere, garbage may now have been generated, for the first memory set aside for thingy no longer has this reference (unless it has been assigned.) For traced objects, this is of no consequence. The garbage collector merely takes note of the fact that this reference no longer accesses the memory in question, and when it next activates, if it finds the memory has no references at all, the memory is collected.

To illustrate a few of the things that cannot be done, consider the following (erroneous) module.

MODULE TestObjectErrors;

CLASS Bad; (* because it is not marked as traced *)
END Bad;

  MODULE NotAllowed; (* only const, var, type and procedure *)
  END NotAllowed;
    MODULE Allowed; (* but a dynamic module in method is ok *)
    END Allowed;
  END Hit;

END One;
    secret = 3;      
END Two;

  badConstant1 = 2 * secret; (* didn't reveal secret *)

  REVEAL secret, thingy;
    secret = 3;
   thingy: CARDINAL;
  PROCEDURE Empty ();
  END Empty;     
END Three;

  badConstant2 = 2 * secret; (* even if we had, it can't be referred to like this *)
  goodConstant = 2 * Three.secret; (* but rather this way *)
  three : Three;
  badConstant3 = 2 * three.secret; (* reference to such a constant not allowed in constant expression *)

  REVEAL inClassType;
    inClassType = CARDINAL;      
END Four;

  mine : Four.inClassType; (* ok to do this with a type *)
  four : Four;
  another : four.inClassType; (* but base language rules don't allow this *)
  mine := 5;
  mine := three.secret; (* this reference OK *)
  Three.thingy := 5; (* can't refer to a variable by class name*)
  Three.Empty ();  (* or to a method, whether revealed or not *)
  three.thingy := 5; (* can refer to a variable by object name *)
  three.Empty ();  (* or to a method *)
END TestObjectErrors.

When this piece of "code" was fed to the compiler, it responded as follows:

#    3  CLASS Bad; (* because it is not marked as traced *)
#####           ^ 188: untraced classes not allowed in safeguarded modules
 File "TestObjectErrors.MOD"; Line 3
#    7    MODULE NotAllowed; (* only const, var, type and procedure *)
#####          ^ 173: module declaration not allowed in class declaration
 File "TestObjectErrors.MOD"; Line 7
#   23    badConstant1 = 2 * secret; (* didn't reveal secret *)
#####                             ^  73: identifier not declared
 File "TestObjectErrors.MOD"; Line 23
#   36    badConstant2 = 2 * secret; (* even if we had, it can't be referred to like this *)
#####                             ^  73: identifier not declared
 File "TestObjectErrors.MOD"; Line 36
#   41    badConstant3 = 2 * three.secret; (* reference to such a constant not allowed in constant expression *)
#####                            ^  74: wrong class of identifier
 File "TestObjectErrors.MOD"; Line 41
#   54    another : four.inClassType; (* but base language rules don't allow this *)
#####                  ^  74: wrong class of identifier
 File "TestObjectErrors.MOD"; Line 54
#   59    Three.thingy := 5; (* can't refer to a variable by class name*)
#####                ^ 170: variable attributes cannot be accessed via class type
 File "TestObjectErrors.MOD"; Line 59
#   60    Three.Empty ();  (* or to a method, whether revealed or not *)
#####               ^ 171: entity not revealed in defining class
 File "TestObjectErrors.MOD"; Line 60
#   62    three.Empty ();  (* or to a method *)
#####               ^ 171: entity not revealed in defining class
 File "TestObjectErrors.MOD"; Line 62
Modula2 - Execution terminated!
### MPW Shell - Execution of makeout terminated.

The reasons for most of the error messages should be obvious in the light of the discussion. One that may escape the reader with no previous experience of objects is the second to last one. Just as the use of items from an unrefined generic module on their own is meaningless, so also is the use of variables and methods qualified by only their class name outside the class. However, it is acceptable to refer to constants and types this way. Only if an object of class Three has been instantiated with a declaration and CREATE does it make sense to refer to Entity.secret. No reference to Secret on its own is valid outside the class declaration.

Experimentation (or the writing of bad code) ought to reveal some other things that are forbidden--specifically things that might threaten a traced object reference.

One cannot:

One cannot in a safeguarded module:

If the programmer does subvert the safety of traced objects using something from the module SYSTEM to obtain a reference to a traced object in an unsafeguarded module, the reference so obtained is not tracked by the garbage collector, and the validity of the program becomes questionable (a supposedly traced object has an untraced reference). Such attempts should not therefore be made.