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.
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;
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.
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.
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.
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.
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. |
Like "SYSTEM", "GARBAGECOLLECTION" is also a pseudo-module and therefore does not have a symbol file in p1 Modula-2.
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.
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).
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.
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.
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.
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) |