3 Extensions to the Language by ISO

With version 7.0 of p1 Modula-2 the language extensions described in the ISO standards 10514-2 and 10514-3 (generics and object oriented programming) became available. This section contains a brief description of these extensions, please see the standards for more details.

3.1 Objects

This section documents the language extensions for object oriented programming and provides a short introduction to object-oriented programming. A detailed explanation of objected-oriented programming concepts can be found in the literature (cf. chapter 2). Advanced examples can be found in the folder "Examples". There is no standard terminology for object-oriented language constructs; the following description uses the terminology of the ISO Standard.

Classes are an extension of the structured types of Modula-2. Similar to record types a class consists of data fields (called attributes) of any type. Additionally, a class may have type declarations and constant declarations of its own as well as procedures that operate on the attributes. These "class procedures" are called methods. All items declared in a class are called entities of that class.

The most interesting feature of classes, however, is the concept of inheritance. You can define one class as a customization of another class. When one class is based on another, the original is called the ancestor and the newly defined one the descendant. A descendant type inherits all the entities of its ancestor; new entities can also be added. Furthermore you can override the implementation of an inherited method, in order to adapt its behaviour appropriately for the descendent. However you cannot change an inherited method's interface (i.e. its parameter list).

In the following the terms son class and father class are used also for grandchildren / grandparents. It is stated explicitly, if the direct descendant / ancestor is addressed.

Variables of class types are used to store references to values of class type (called objects). Objects are created (instantiated) dynamically; they are also destroyed dynamically (cf. 3.1.2).

The model described in IS 10514-3 has single inheritance and reference semantics (i.e. variables and parameters of class type contain a reference to the object, only this reference is assigned). Classes look like modules, so the visibility rules common for Modula-2 apply also to entities of a class. The model provides for automatically executed constructors and destructors (not available for "TRACED CLASS"es, cf. 3.1.2) and for inquiries on the hierarchical position of a class in the inheritance tree.

The syntax is completely contained in appendix I.

3.1.1 Class Declaration

A class declaration is possible only at global level. In most parts it is similar to a module declaration, only the key word "MODULE" is replaced by "CLASS". Within a class declaration all constants, types, attributes and methods are defined. In contrary to a module a class declaration does not need imports, as within the scope of a procedure all names declared outside are visible.

Further all names declared in ancestor classes are visible, they belong however to the scope of the current class and therefor may not be reused. Only for methods a redefinition is possible by using the key word "OVERRIDE", the parameter list needs to remain identical. The overridden method may be accessed by use of the qualification "ClassName. MethodName" The "INHERIT"-statement allows to specify exactly one class as father class (single inheritance). Names that shall be usable outside the class have to be made explicitly visible by use of the "REVEAL"-statement. Only names of the own class, not names declared in a father class, may be revealed. Attributes may be preceded by "READONLY", in this case only reading access is possible.

Classes may be declared completely within a program or implementation module. The declaration may be split in definition part (in a definition module) and declaration part (within the according implementation module). For exported methods—like with procedures—only the headers are specified in the definition module; they have to be implemented in the declaration part of the class. A possible father class is specified in the definition part only. All names revealed in the definition part are automatically revealed in the declaration part, further names may be revealed.

Like modules, classes may have initialization and finalization part (cf. 3.1.2). These act as constructor / destructor and are automatically executed (nested) at object instantiation / destruction.

During instantiation a father constructor is executed before a son constructor, i.e. the fathers are completely initialized before the own constructor is executed. Please note that for each constructor the methods are dispatched according to the static type the constructor belongs to; overriding by son classes is not yet recognized in order to avoid access to uninitialized attributes.

At destruction of an object the destructors are executed in reverse order, the son destructor before the father destructor. Methods are dispatched according to the dynamic type of the object.

Examples for classes (definition part):

ABSTRACT CLASS TShape;
REVEAL RandomRect, Move, Draw, Erase;
VAR
    boundRect: Rect;     (*bounding box*)
PROCEDURE RandomRect;    (*Assign a random rectangle to boundRect*)
PROCEDURE Move;          (*Assign a different random rectangle to boundRect*)
ABSTRACT PROCEDURE Draw (pat: Pattern);
ABSTRACT PROCEDURE Erase;
END TShape;

CLASS TArc;
INHERIT TShape;
VAR
    startAngle, arcAngle: INTEGER;
OVERRIDE PROCEDURE Draw (pat: Pattern);
OVERRIDE PROCEDURE Erase;
END TArc;

CLASS TRoundRect
INHERIT TShape;
VAR
    ovalWidth,
    ovalHeight: INTEGER;   (*curvature of rounded corners*)
OVERRIDE PROCEDURE Draw (pat: Pattern);
OVERRIDE PROCEDURE Erase;
END TRoundRect;

In this example three classes are defined, "TArc" and "TRoundRect" being descendants of "TShape". "TShape" defines a general graph object with enclosing rectangle as attribute and some methods. The methods "Draw" and "Erase" are defined as interface without implementation (cf. 3.1.2), they are implemented in son classes by use of "OVERRIDE". So it is possible to implement the common administration part for graph objects of type "TShape" without needing to know the properties of the special shapes "TArc" or "TRoundRect". Moreover this part of the program needs no change if other shapes are added as descendants of "TShape". The methods "Draw" and "Erase" may be used without knowledge of their particular implementation. It is not possible (checked by the compiler) to instantiate an object of class "TShape".

Examples for classes (declaration part):

ABSTRACT CLASS TShape;

PROCEDURE RandomRect;    (*Assign a random rectangle to boundRect*)
BEGIN
    (* Assign some Value to boundRect *)
END RandomRect;

PROCEDURE Move;          (*Assign a different random rectangle to boundRect*)
BEGIN
    Erase;
    RandomRect;
    Draw;
END Move;

BEGIN
    RandomRect;
FINALLY
    Erase;
END TShape;

CLASS TRoundRect

OVERRIDE PROCEDURE Draw (pat: Pattern);
BEGIN
    FillRoundRect (boundRect, ovalWidth, ovalHeight, pat); (*Quickdraw*)
    FrameRoundRect (boundRect, ovalWidth, ovalHeight); 
END Draw;

OVERRIDE PROCEDURE Erase;
BEGIN
    EraseRoundRect (boundRect, ovalWidth, ovalHeight);
END Erase;

BEGIN
    ovalWidth := 20;
    ovalHeight := 15;
END TRoundRect;

"REVEAL" and "INHERIT" are already specified in the definition part and are not to be specified twice. Constructors and destructors are implemented.

Additionally to the explicit parameters each method has an implicit parameter named "SELF", "SELF" contains a reference to the object used for invoking the method. The (static) class of "SELF" is the class the method is declared in. Like with other object variables this need not be the real (dynamic) type of the referenced object; the object may be of some son type.

The parameter "SELF" may only be used read-only. Assignments to "SELF" are illegal.

If two classes make use of each other, one class has to be declared anonymous in advance (like with pointers), i. e. only the name of the class is known. This is done by replacing the body by the key word "FORWARD":
CLASS FirstClass;
FORWARD;

3.1.2 Kinds of Classes

With respect to the end of the life time of an object the programmer may choose between two kinds of classes in ISO Modula-2. For "normal" classes ("CLASS MyClass") an object is destroyed by use of the predefined procedure "DESTROY". For "traced" classes ("TRACED CLASS MyClass") the object is destroyed (or better marked for destruction) implicitly as soon as it is no longer referenced (garbage collection). This implies that the storage for objects of traced classes is supplied and administrated by the runtime system; objects of normal classes may use any storage. Traced classes do not have a destructor, as it is not possible to tell when this destructor would be executed.

For all objects instantiation is done by the predefined procedure "CREATE":

VAR
    obj: TArc;
  :
CREATE (obj);

For instantiation of a normal class, like with "NEW", a procedure "ALLOCATE" needs to be visible. It is used to allocate storage for the object. If enough storage is available, the constructor chain is executed and a reference to the object is returned in "obj". Otherwise "obj" contains the value "EMPTY" after the execution of "CREATE". On instantiation of an object of a traced class storage is provided for by the runtime system. In any other respect the semantics of "CREATE" is the same as for untraced classes. The call:

VAR
    obj: TShape;
  :
CREATE (obj, TArc);

creates an object of type "TArc". "TArc" has to be a descendant of "TShape".

For untraced classes an object is destroyed by:
DESTROY (obj);
First the destructor chain is executed. Then the storage is released. Like with "DISPOSE" a procedure "DEALLOCATE" with the expected parameters needs to be visible.

For the specification of general interfaces "abstract" variants of both kinds of classes may be used ("ABSTRACT CLASS", "TRACED ABSTRACT CLASS"). Within abstract classes methods need not to be implemented. For these abstract methods (flagged by "ABSTRACT" before the key word "PROCEDURE") only the header is specified for complete description of the interface. Within descendants these methods are overridden and implemented. Because of the missing methods it is not possible to instantiate an object of an abstract class (cf. examples in section 3.1.1).

Modules that contain untraced classes have to be preceded by the key word "UNSAFEGUARDED". This flags that within these modules dangling referenced may be caused by wrongful destruction of (still used) objects.

3.1.3 Use of Object Variables

Attributes and methods are (similar to the fields of a record) accessed by selection of an object variable (constants and types may be accessed by class name).

Object variables are assignment compatible to all variables of ancestor classes. This implies that the static type used to declare an object variable need not be the real (dynamic) type of the referenced object; the object may be of any son type. When calling a method it is not generally possible to determine statically which method implementation is to be used; the implementation of some overriding son class may be used. The empty reference ("EMPTY") is assignment compatible to all classes.

Object variables may be compared to each other if they are of the same class or one is a descendant of the other ("=", "<>"). Equality / inequality of the object reference, not of the object itself is tested (i.e. equality means that both variables reference the same object). All object variables may be compared to "EMPTY".

The standard function "ISMEMBER" allows to check at run time whether an object or a class is compatible to another object or class. The parameters of "ISMEMBER" are two objects or classes. "ISMEMBER" returns "TRUE" if the type of the first parameter is of the same type as the second parameter or a descendant of that type. This is equal to the test whether the first parameter is assignment compatible to the second parameter.

The newly introduced "GUARD"-statement provides for checking and access to the dynamic type of an object. The syntax of a "GUARD"-statement is similar to the syntax of the "CASE"-statement. Within a "GUARD"-statement the dynamic type of an object is compared sequentially (notation order) to the specified classes. The statement sequence is executed that follows the fist class the object is assignment compatible to.

Example:

GUARD obj AS
  obj1: TArc DO
    (* executed iff obj is of type TArc or a son class *)
| obj2: TRoundRect DO
    (* executed iff obj is of type TRoundRect or a son class *)
| : TShape DO
    (* executed iff obj is of type TShape or a son class that
       is not descendant of TArc or TRoundRect
    *)
ELSE
    (* executed iff obj is of any other class *)
END;

The (optional) object variables "obj1" respectively "obj2" contain the same reference as "obj", but they have the static (and asserted) type of the given class. By use of these variables additional attributes / methods of that class may be accessed (if revealed, cf. "REVEAL"). "obj1" and "obj2" are read-only (like "SELF").

The "ELSE" part may be omitted. In this case an exception is raised, if no class matches (cf. 3.1.4).

3.1.4 The Module "M2OOEXCEPTION"

The object oriented extensions define several new exceptions. This module provides the data types and procedures for testing the new exceptions (cf. 2.7).

Like "SYSTEM" the module "M2OOEXCEPTION" is a pseudo module and therefore does not have a symbol file in p1 Modula-2.

3.1.4.1 The Pseudo Definition Module "M2OOEXCEPTION"

The module can be considered as having the following definition module:
DEFINITION MODULE M2OOEXCEPTION;

(* Provides facilities for identifying exceptions of the extended language *)

TYPE
  M2OOExceptions =
    (emptyException, abstractException, immutableException, guardException);

PROCEDURE M2OOException (): M2OOExceptions;
  (* If the current coroutine is in the exceptional execution state because
     of the raising of an exception of the language extensions, returns the
     corresponding enumeration value, and otherwise raises an exception.
  *)

PROCEDURE IsM2OOException (): BOOLEAN;
  (* If the current coroutine is in the exceptional execution state because
     of the raising of an exception of the language extensions, returns
     TRUE, and otherwise returns FALSE.
  *)

END M2OOEXCEPTION.

3.1.4.2 Description

M2OOExceptions
The type "M2OOExceptions" identifies all exception conditions defined in the standard for OO-extensions. It is the result type of "M2OOException", the function used to find out the cause of an exception. (see also section 2.7.3.4).

M2OOException
"M2OOException" is used to find out which standard exception has been raised. If no exception of the OO-extensions has been raised, the exception "exException" is raised.

IsM2OOException
"IsM2OOException" is used to find out whether the cause of the exception is one defined in the OO-extensions or not.

3.1.4.3 Exception Conditions for "OO-Extensions"

emptyException This exception is raised on the attempt to deference an object variable that contains the value "EMPTY".
abstractException Execution of an abstract method (only possible while executing the constructor chain).
immutableException Writing access of a variable available only for reading access ("READONLY"-Attribute, "SELF", selector variable in a "GUARD"-statement; at run time not detected by p1 Modula-2).
Note: Changing an immutable variable can only achieved by (mis-)use of SYSTEM facilities; an assignment to an immutable variable is detected by the compiler and results in a compilation error.
guardException The selector expression of a "GUARD"-statement does not match with any of the classes in the alternatives and "ELSE" is not specified.

3.1.5 The Module "GARBAGECOLLECTION"

This module provides for administration of the storage system for traced classes; in particular the garbage collector.

Like "SYSTEM", "GARBAGECOLLECTION" is also a pseudo-module and therefore does not have a symbol file in p1 Modula-2.

3.1.5.1 The Pseudo Definition Module "GARBAGECOLLECTION"

The module "GARBAGECOLLECTION" could have a definition module something like this:
DEFINITION MODULE GARBAGECOLLECTION;

(* Provides facilities for controlling the garbage collector. *)

PROCEDURE IsCollectionEnabled (): BOOLEAN;
  (* If garbage collection is enabled then returns TRUE and otherwise
     returns FALSE.
  *)

PROCEDURE SetCollectionEnable (on: BOOLEAN);
  (* If on is TRUE then enable garbage collection; otherwise if on is
     FALSE and garbage collection can be disabled then disable garbage
     collection.
  *)

PROCEDURE ForceCollection;
  (* If garbage collection can be forced then force it else do nothing. *)

END GARBAGECOLLECTION.

3.1.5.2 Description

IsCollectionEnabled
Returns "TRUE" if the garbage collector may release storage, "FALSE" otherwise.

SetCollectionEnable
A parameter value of "TRUE" allows the garbage collector to release memory. A parameter value of "FALSE" disallows the garbage collector to release memory if the garbage collector provides for being switched off.

ForceCollection
Frees all unreferenced objects (optional feature).

3.2 Generics

Many programming constructs like lists, trees, general sets etc. are defined only by their structure, not the data stored within. Generics provides for the possibility to generally construct such procedures / packages and refine them for the desired data structures, i.e. to compile them with specified types.

To achieve a most general mechanism, not only types have to be parameterized, but also constants, in particular, procedures (e.g. the comparison procedures for the given data type in sorting algorithms).

Because of the language structure of Modula-2, the level of modules is most appropriate for generic units. A generic unit is implemented as definition / implementation module pair. The new generic modules have formal parameters for the types and constants to be passed. This pair may be refined as an independent pair definition / implementation modules; it may as well be refined as a local module. On refinement the formal parameters are replaced (semantically like a text replacement) by the actual parameters.

3.2.1 Generic Modules

Generic modules differ from normal modules by the key word "GENERIC" at the start of the module and by the formal parameter list after the module name (respectively after the protection expression). The formal parameter list is very similar to the formal parameter list of a procedure. The formal parameters have either the form "name: type" (open arrays are possible) or "name: TYPE" for type parameters. The parameter list of definition and implementation module have to match.

Example:

GENERIC DEFINITION MODULE Validate (PType: TYPE; PValidProc: ValidProcType);
TYPE
    ValidProcType = PROCEDURE (item : PType) : BOOLEAN;
    (* Note the forward reference in the module parameter list *)
PROCEDURE Valid (item: PType): BOOLEAN;
END Validate.

GENERIC IMPLEMENTATION MODULE Validate
    (PType: TYPE; PValidProc: ValidProcType);

PROCEDURE Valid (item: PType): BOOLEAN;
BEGIN
    RETURN PValidProc (item);
END Valid;

END Validate.

GENERIC DEFINITION MODULE Stacks (Element: TYPE);

CONST
    StackSize = 100;

PROCEDURE Push (item: Element);
PROCEDURE Pop (VAR item : Element);
PROCEDURE Empty (): BOOLEAN;

END Stacks.

GENERIC IMPLEMENTATION MODULE Stacks (Element: TYPE);

VAR
    stack : ARRAY [0 .. StackSize] OF Element;
    stackPtr: CARDINAL;
(* One could also arrange for StackSize to be a parameter. *)

PROCEDURE Push (item: Element);
BEGIN
    stack [stackPtr] := item; 
    INC (stackPtr);
END Push;

PROCEDURE Pop (VAR item: Element);
BEGIN
    DEC (stackPtr);
    item := stack[stackPtr];
END Pop;

PROCEDURE Empty (): BOOLEAN;
BEGIN
    RETURN stackPtr = 0;
END Empty;

BEGIN (* module body initialization *)
    stackPtr := 0;
END Stacks.

GENERIC DEFINITION MODULE Sorts (Item : TYPE; GenCompare : CompareProc);
FROM Comparisons IMPORT CompareResults;
TYPE 
    CompareProc = PROCEDURE (Item, Item) : CompareResults;
PROCEDURE Quick (VAR data : ARRAY OF Item);
(* Other procedures and functions could be included as well. *)
END Sorts.

GENERIC IMPLEMENTATION MODULE Sorts (Item: TYPE; GenCompare: 
CompareProc);
FROM Comparisons IMPORT CompareResults;

PROCEDURE Swap (VAR a, b: Item);
VAR
    temp : Item;
BEGIN
    temp := a;
    a := b;
    b := temp;
END Swap;

PROCEDURE Quick (VAR data: ARRAY OF Item);
BEGIN
    (* typical quicksort algorithm, using "Swap" for item swapping. *)
END Quick;

END Sorts.
Forward references to types later defined in the generic definition module are allowed as types in formal parameters.

3.2.2 Refining as Definition / Implementation Module

Essentially, the refining modules specify the actual parameters and the name for the new module. Refinement is flagged by the sign "=" after the module name; it is followed by the name of the generic module and, if needed, the actual parameters. Refining modules do not have import lists or definitions / declarations of their own.

In the list of actual parameters all names known on global level are available. These are the pervasive names and especially the names of all (available) pairs of definition / implementation modules.

Examples (the module "IntegerInfo" provides for the comparison procedure needed for sorting):

DEFINITION MODULE IntegerInfo;
FROM Comparisons IMPORT CompareResults;
PROCEDURE Compare (i1, i2: INTEGER): CompareResults;
END IntegerInfo.

IMPLEMENTATION MODULE IntegerInfo;
FROM Comparisons IMPORT CompareResults;

PROCEDURE Compare (i1, i2: INTEGER): CompareResults;
BEGIN
    IF i1 < i2
    THEN
        RETURN less;
    ELSIF i1 > i2
    THEN
        RETURN greater;
    ELSE
        RETURN equal;
    END(*IF*);
END Compare;

END IntegerInfo.

DEFINITION MODULE IntSorts = Sorts (INTEGER, IntegerInfo.Compare);
END IntSorts.

IMPLEMENTATION MODULE IntSorts = Sorts (INTEGER, IntegerInfo.Compare);
END IntSorts.

3.2.3 Refinement as Local Module.

When refining as local module, the generic definition and implementation module are merged to a single unit (the definition module may be viewed as containing forward declarations for the procedures etc. defined there).

The syntax for the refining module is equal to a similar refining implementation module. A refining local module does also not have imports or declarations. In contrast, a refining local module has an export list of its own, especially to define the kind of export (qualified / unqualified). The name of the generic module has to be visible at the place of refinement, in particular, the generic module has to be imported.

Generic modules may contain local refinements.

Example (the module "ValidStacksClient" is built on top of the generic module "ValidStacks" which in his turn is built on top of "Stacks" and "Validate"):

GENERIC DEFINITION MODULE ValidStacks
        (PType: TYPE; PValidProc: ValidProcType);

    TYPE
        ValidProcType = PROCEDURE (PType): BOOLEAN;

    PROCEDURE PushValid (item: PType);

END ValidStacks.

GENERIC IMPLEMENTATION MODULE ValidStacks
        (PType: TYPE; PValidProc: ValidProcType);

    IMPORT Stacks, Validate;

    MODULE MyStacks = Stacks (PType);
        EXPORT QUALIFIED StackSize, Push, Pop, Empty;
    END MyStacks;

    MODULE MyValidate = Validate (PType, PValidProc);
        EXPORT QUALIFIED Valid;
    END MyValidate;

    PROCEDURE PushValid (item: PType);
    BEGIN
        IF MyValidate.Valid (item) 
            THEN
                MyStacks.Push (item)
        END(*IF*);
    END PushValid;

END ValidStacks.

MODULE ValidStacksClient;
    IMPORT ValidStacks, Stacks, Validate;

    PROCEDURE MyIntegerTestProc (i: INTEGER): BOOLEAN;
    BEGIN
        RETURN TRUE;
    END MyIntegerTestProc;

    MODULE ValidIntegerStack = ValidStacks (INTEGER, MyIntegerTestProc);
        EXPORT PushValid;
    END ValidIntegerStack;

BEGIN
    PushValid (5);
END ValidStacksClient.

chapter 2 (compiler) start page chapter 4 (compiler)