In-Depth
Extensions to the Modula-2 Language
The Modula-2 programming language was recently extended with OO and generic programming facilities. Both the language and the extensions were standardized by the ISO/IEC. We provide a short introduction to the new facilities.
A recent article in JOOP1 discussing OO extensions to Ada '95 inspired this article. We show how similar features were designed, implemented, and standardized for the Modula-2 language.2,3 Before introducing the new features we give a short introduction to the Modula-2 standard.4 We then discuss the recently standardized OO and generic extensions to the language.
These extensions are orthogonal to each other and to the Modula-2 base language, which can be extended optionally with one or both extensions with minimal change. The only change to the lexical structure of the base language has been the addition of several new key words. This change will only invalidate the very small number of programs that have been using the new key words as identifiers.
We do not give a complete syntactic and semantic overview of these extensions here. Readers interested in an overview are referred to the available literature,5,6 the international standards,7,8 and the Modula-2 Web site of WG13.9
STANDARD MODULA-2
Modula-2 was standardized by ISO/IEC JTC1/SC22/WG13, an international working group consisting of people from industry and academia, and users of the language. The standard is unique because it contains a complete formal definition of the language using the formal specification language, Vienna Development Method-Specification Language (VDM-SL).10 Experiences with the specification language and the standardization process were reported in "Constructing formal language definitions..."5 The construction of standard conforming compilers has benefited from the formal definition because questions on the interpretation of unclear language constructs can be easily answered using the document. Despite the advantages of having a formal definition, there was a decision not to produce one for the language extensions described here. One reason behind this decision was the effort needed to produce such a formal definition. The second and more important reason was that, although the OO extensions are lexically and syntactically pure extensions, semantically they are not. Defining the type structure of the OO extensions would have required rewriting the definition of the type structure of the base language. The lack of composition in such formal definitions was commented upon in the aforementioned article.5 The OO and generic extensions have therefore been defined in an extended form of EBNF11 and English prose.
THE OO EXTENSION
To enable programming in an OO style, a class construct was added to the Modula-2 language. Although classes duplicate some of the encapsulation facilities already available in the base language, it was decided that the upward compatibility achieved by having encapsulation both in the base language and in the class construct was preferable to reducing the duplication by moving toward an Oberon-like model with record extensions.12
The OO model chosen is quite conservative and can be characterized as follows:
- Objects can only be dynamically allocated; access to objects is via references only.
- Single inheritance is considered sufficient for the intended application area.
- Classes are introduced as a new syntax construct and can be used as any other type. Two types of classes are distinguished: traced classes, where unreferenced objects are collected by a garbage collector, and untraced classes, where objects have to be destroyed manually. The syntax of a class is similar to that of a module:
CLASS MyClass;
BEGIN
FINALLY
END MyClass;
The declaration section may contain variable declarations (i.e., declarations of attributes) and procedure declarations (i.e., declarations of methods), as well as constant and type declarations. All declarations are subject to the visibility rules stated in the next section.
The class body establishes the constructor in its initialization part and the destructor in its finalization part, except in traced classes. Constructors in inheritance trees are automatically invoked from root to leaf upon object creation; destructors are executed upon destruction from leaf to root.
A class can inherit from, at most, one other class. Inheritance is specified directly after the class header:
CLASS MyChildClass;
INHERIT MyClass;
A child class may redeclare methods already declared in the parent class by overriding them. An overriding method must have the same parameter lists as the overridden method (no covariance). Overriding is specified by the keyword OVERRIDE before the method header. Access to the overridden method is possible by the qualification parentClassname.methodname.
- Visibility can be distinguished as hidden, family (for subclasses), or public. By default, items declared inside of a class are visible only to that class and its descendents (family visible). If these items are to be available to the clients of that class they have to be revealed. Attributes may be revealed read-only, i.e., they may be read from but not written to. The reveal statement follows the inherit clause (or class header, if there is no inherit clause):
CLASS MyClass;
REVEAL Method1, attribute1, READONLY attribute2;
- Modula-2 provides traced and untraced classes. Objects instantiated from traced classes are safeguarded from dangling object reference errors unless the safeguarding mechanism is deliberately subverted. Traced objects are automatically initialized upon their creation and are automatically subject to being reclaimed by the garbage collector when they are no longer referenced. Traced classes are declared by the keyword TRACED. For example:
TRACED CLASS MyTracedClass;
For untraced classes (default), the programmer is responsible for the destruction of no longer used objects. Compilation modules containing untraced objects or threatening (unsafe) constructs should be tagged as UNSAFEGUARDED For example:
UNSAFEGUARDED MODULE MyModule1;
- A class may be flagged abstract. Inside of these classes, abstract methods having only a signature but no implementation can be declared. Because the purpose of abstract classes is to declare interface descriptions, no object can be instantiated from them. Only complete child classes can be instantiated:
TRACED ABSTRACT CLASS MyInterface;
:
PROCEDURE INIT;
ABSTRACT PROCEDURE Display;
:
- Class declarations can be used in both program modules and implementation modules. A class definition can occur in the corresponding definition module. Such a class definition specifies the external interface of the class. This interface consists of the exported types, constants, variables, and method signatures. If a class definition exists, inheritance has to be specified in the definition, not in the declaration. Abstract and traced flags have to be specified in both a class definition and class declaration. An abstract class has to be specified in a class definition (otherwise it will be impossible for subclasses in other modules to implement it). All items defined in a class definition are visible to subclasses in other modules. Items declared in an implementation module are hidden from view in other modules by the Modula-2 default scope rules.
- The procedure CREATE is used to instantiate an object. After successful memory allocation, the constructor chain of a class (as explained earlier) is invoked. For untraced classes, an ALLOCATE procedure must be visible at the point of call to CREATE (as with NEW in the base language). For traced classes, a runtime procedure known by the garbage collector is used for memory allocation.
For untraced classes, an object can be destroyed by a call to DESTROY, but DEALLOCATE has to be visible. Before deallocating the memory, the destructor chain is executed.
Objects of traced classes are collected automatically by the garbage collector when no longer referenced. They are not allowed to be destroyed manually nor do they have a destructor, i.e., they may not have a FINALLY clause.
- There are two facilities for testing class membership:
ISMEMBER (object_or_class1, object_or_class2) returns TRUE object_or_class1 is equal to or a descendant of the dynamic type of object_or_class2.
The newly introduced guard statement provides access to the features of the dynamic type of an object.
GUARD myObj AS
obj1: Class1:
< access="" to="" all="" features="" of="" class1="" via="" the="" local="" variable="" obj1="">
| obj2: Class2:
< access="" to="" all="" features="" of="" class2="" via="" the="" local="" variable="" obj2="">
ELSE
< something="" else="">
END;
The dynamic type of myObj is checked against the types in the given variants. The first variant, the class of myObj is equal to or a subtype of, is chosen. If no variant matches, and no ELSE clause is given, an exception will be raised.
- A system module GARBAGECOLLECTION has been added to control the garbage collector, and a system module M2OOEXCEPTION has been added to deal with new exceptions.
The following code extracted from a drawing application clearly explains the aforementioned and is given with comments explaining the constructs used. The example also shows several calls to the I/O-library of ISO-standard Modula-2.
UNSAFEGUARDED DEFINITION MODULE Drawing;
IMPORT MyWindows;
ABSTRACT CLASS DrawObject;
(* DrawObject is an abstract class *)
(* Its children may be concrete classes *)
REVEAL PlotInit, DrawTo, DrawAxis;
(* all attributes are for subscribers only *)
VAR
xMin, xMax, yMin, yMax,
xZero, yZero, xAct, yAct: INTEGER;
xFactor, yFactor: REAL;
(* methods to initialize a (new) coordinate system *)
PROCEDURE PlotInit (left, right, bottom, top: INTEGER;
vleft, vright, vbottom, vtop: REAL);
(* draw methods *)
PROCEDURE DrawTo (x, y: REAL);
(* miscellaneous *)
ABSTRACT PROCEDURE DrawAxis (withScale: BOOLEAN);
(* for subscribers only *)
ABSTRACT PROCEDURE DoDrawing (x, y: INTEGER;
toDraw: BOOLEAN);
PROCEDURE PointOk (x, y: INTEGER): BOOLEAN;
END(*CLASS*) DrawObject;
(* procedures to create new objects; the user need not know
about the object hierarchy *)
PROCEDURE CreateScreenObject
(window:MyWindows.WindowHandle): DrawObject;
PROCEDURE CreatePlotterObject (fileName: ARRAY OF CHAR):
DrawObject;
PROCEDURE CreatePrinterObject (): DrawObject;
END Drawing.
The corresponding implementation module shows the bodies of the nonabstract methods. The implementation module does not show the
DrawAxis and
DoDrawing abstract methods. These methods will become concrete in a child class.
UNSAFEGUARDED IMPLEMENTATION MODULE Drawing;
IMPORT MyWindows, DrawingScreen, DrawingPrinter,
DrawingPlotter;
FROM Storage IMPORT ALLOCATE;
FROM RealMath IMPORT sin, cos, round;
ABSTRACT CLASS DrawObject;
(* private attributes *)
VAR
lastIn, (* TRUE, iff the last used point
is inside the coordinate system *)
initialized: BOOLEAN; (* TRUE, iff an init method was called *)
(* see PlotInit *)
(* methods to initialize a (new) coordinate system *)
PROCEDURE PlotInit (left, right, bottom, top: INTEGER;
vleft, vright, vbottom, vtop: REAL);
BEGIN
Xmin := left * resolution;
XMax := right * resolution;
YMin := bottom * resolution;
YMax := top * resolution;
XFactor := FLOAT (xMax - xMin) / (vright - vleft);
yFactor := FLOAT (yMax - yMin) / (vbottom - vtop);
xZero := (xMin - round (vleft * xFactor));
yZero := (yMin - round (vbottom * yFactor));
xAct := xMin;
yAct := yMin;
initialized := TRUE;
END PlotInit;
(* draw methods *)
PROCEDURE DrawTo (x, y: REAL);
VAR
xNew, yNew: INTEGER;
BEGIN
IF NOT initialized THEN
RETURN;
END(*IF*);
xNew := xZero + ConvToAbs (x * xFactor);
yNew := yZero + ConvToAbs (y * yFactor);
lastIn := PointOK (xNew, yNew);
IF lastIn THEN
DoDrawing (xNew, yNew, lastIn);
END(*IF*);
xAct := xNew;
yAct := yNew;
END DrawTo;
(* for subscribers only *)
PROCEDURE PointOk (x, y: INTEGER): BOOLEAN;
BEGIN
RETURN (x >= xMin) AND (x <= xmax)="" and="" (y="">= yMin) AND (y <= ymax);="" end="" pointok;="" (*="" private="" *)="" procedure="" convtoabs="" (virt:="" real):="" integer;="" begin="" if="" virt="">= 2000000000.0 THEN
RETURN 2000000000;
ELSIF virt <= -2000000000.0="" then="" return="" -2000000000;="" else="" return="" int="" (virt);="" end(*if*);="" end="" convtoabs;="" begin="" (*="" constructor="" *)="" lastin="" :="FALSE;" initialized="" :="FALSE;" end="" drawobject;="" procedure="" createscreenobject="" (window:="" mywindows.windowhandle):="" drawobject;="" var="" new:="" drawingscreen.screenobject;="" begin="" create="" (new);="" new.initdata="" (window);="" return="" new;="" end="" createscreenobject;="" procedure="" createplotterobject="" (filename:="" array="" of="" char):="" drawobject;="" var="" new:="" drawingplotter.plotterobject;="" begin="" create="" (new);="" new.initdata="" (filename);="" return="" new;="" end="" createplotterobject;="" procedure="" createprinterobject="" ():="" drawobject;="" var="" new:="" drawingprinter.printerobject;="" begin="" create="" (new);="" return="" new;="" end="" createprinterobject;="" end="" drawing.="">=>=>=>
The
DrawingPlotter module describes a
PlotterObject class. This class is one possible child of the
DrawObject class previously mentioned.
UNSAFEGUARDED DEFINITION MODULE DrawingPlotter;
IMPORT Drawing;
CLASS PlotterObject;
INHERIT Drawing.DrawObject;
REVEAL InitData; (* The concrete class may of course
extend the parent class with new methods.
These new methods may be REVEALED *)
OVERRIDE PROCEDURE DrawAxis
(withScale: BOOLEAN);
OVERRIDE PROCEDURE DoDrawing
(x, y: INTEGER; toDraw: BOOLEAN);
(* does the necessary initialization *)
PROCEDURE InitData (name: ARRAY OF CHAR);
END PlotterObject;
END DrawingPlotter.
An incomplete implementation of the
DrawingPlotter module containing the bodies of the overridden
DrawAxis and
DoDrawing methods together with the new
InitData method is as follows:
UNSAFEGUARDED IMPLEMENTATION MODULE DrawingPlotter;
FROM SeqFile IMPORT old, ChanId,
OpenResults, OpenWrite, Close;
FROM IOChan IMPORT InvalidChan;
FROM TextIO IMPORT WriteString, WriteChar,
WriteLn;
FROM WholeIO IMPORT WriteInt;
FROM RealStr IMPORT RealToFixed;
FROM WholeStr IMPORT IntToStr;
FROM Strings IMPORT Insert;
FROM RealMath IMPORT sin, cos, round, power, ln;
CLASS PlotterObject;
CONST
unitsPerDot = 100;
VAR
file: ChanId;
characterDistance, color: CARDINAL;
OVERRIDE PROCEDURE DrawAxis (withScale: BOOLEAN);
BEGIN
(* Do drawing of coordinate axis *)
END DrawAxis;
OVERRIDE PROCEDURE DoDrawing (x, y: INTEGER;
toDraw: BOOLEAN);
BEGIN
IF toDraw THEN
Out2 ("D", x / unitsPerDot, y / unitsPerDot);
ELSE
Out2 ("Mv", x / unitsPerDot, y / unitsPerDot);
END(*IF*);
END DoDrawing;
PROCEDURE InitData (name: ARRAY OF CHAR);
VAR
res: OpenResults;
BEGIN
OpenWrite (file, name, old, res);
OutText (':');
Out2 ('M', xMin, yMin);
END InitData;
BEGIN
characterDistance := 28;
color := 0;
file := InvalidChan ();
FINALLY
IF file <> InvalidChan () THEN
Close (file);
END(*IF*);
END PlotterObject;
END DrawingPlotter.
THE GENERIC EXTENSION
The Modula-2 language, as defined by Wirth, lacked a mechanism for parameterized (abstract) data types and parameterized routines, such as sorting routines. The mechanism described here adds such facilities to the language in a nonintrusive way that is orthogonal and supplementary to both the base language and the OO extensions. It is possible to combine the two extensions to provide for object classes encapsulated in, and depending on, the parameters of generic separate modules. This allows for refinement and instantiation of similar objects with different internal data structures.
13
Genericity has been defined on two levels: the level of separate module pairs (definition + implementation module) and the level of local modules (nested as provided for in the base language).
On the first level, two types of modules were added: generic separate module pairs (generic definition and implementation modules) and refining separate module pairs (refining definition and implementation modules).
Generic separate modules are similar to normal definition and implementation modules with the following additions:
- The modules are preceded by the new key word GENERIC.
- The module header is optionally followed by a formal parameter list.
- The formal parameters can be types or constants.
- Such types and constants can be used in the body of the generic module.
- Refining separate modules will supply the actual parameters needed to refine the generic modules.
In the compilation model, the compiler will take a generic module pair and a refining module pair and produce a base-standard-compatible module pair from it as shown in a later example.
Generic facilities have also been made available on the level of local modules. The input to the refinement process is a standard generic module pair and a local refiner. This refiner is located where the refined local module is going to exist.
The compiler substitutes the actual module parameters for the formal ones and generates a single local module from the generic module pair. Because local modules can have export clauses (which separate modules do not have), the syntax allows the programmer to insert these. We do not give an example here, but refer readers to Sigplan Notices.6
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;
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
stackPtr := 0;
END Stacks.
A refiner constructing a stack of cardinal numbers reads as follows:
DEFINITION MODULE CardStack = Stacks(CARDINAL);
END CardStack.
IMPLEMENTATION MODULE CardStack = Stacks(CARDINAL);
END CardStack.
CONCLUSION
The Modula-2 language was updated with OO and generic programming extensions. These extensions were standardized by ISO in 1998. Both extensions have already been successfully implemented in the p1 compiler for the MacOS MPW
14 and in the StonyBrook compiler for the Win32 environments.
15 With these extensions in place, the language can successfully compete in dependable real-time systems and computer education (niche) markets.
References
- Riehle, R. "Java Applets in Ada," JOOP, 11(3): 72–75, Jun. 1998.
- Wirth, N. Programming in Modula-2, third corrected edition, Springer–Verlag, Berlin, Germany, 1985.
- Wirth, N. Programming in Modula-2, fourth corrected edition, Springer–Verlag, Berlin, Germany, 1988.
- ISO/IEC 10514-1 Information Technology, Programming Languages—Modula-2, Base language, ISO/IEC, 1996.
- Pronk, C. and M. Schönhacker. "Constructing formal language definitions, can we manage? What can we learn from Modula-2 standardization," Computer Standards and Interfaces, 19(2): 143–154, Mar. 1998.
- Pronk, C. et al. "Standardized Extensions to Modula-2," Sigplan Notices, 32(11): 34–48, Nov. 1997.
- ISO/IEC 10514-2 Information Technology, Programming Languages—Modula-2, Generics in Modula-2, ISO/IEC, 1998.
- ISO/IEC 10514-3 Information Technology, Programming Languages—Modula-2, Object-oriented Extensions, ISO/IEC, 1998.
- http://sc22wg13.twi.tudelft.nl/.
- ISO/IEC 13817-1 Information Technology, Programming Languages—VDM-Specification Language, Base Language, ISO/IEC, 1996.
- ISO/IEC 14977:1996 Information Technology, Programming Languages—Syntactic Meta Language, Extended BNF, ISO/IEC, 1996.
- Wirth, N. "Type Extensions," Transactions on Programming Languages, 10(2), Apr. 1988.
- Pronk, C. and R. J. Sutcliffe, "Scalable Modules in Modula-2," in Modular Programming Languages, H. Mšssenböck, Ed., No. 1204 in LNCS, Springer–Verlag, Berlin, Germany, 1997.
- http://ourworld.compuserve.com/homepages/Albert_Wiedemann.
- http://www.ehm.kun.nl/~eh/index.html.