FST (Fitted) Modula-2 Version 4.0 Documentation

[Previous]  [Contents]  [Next]

14. OO Extensions

FST Modula-2 adds extensions to standard Modula-2, intended to add support for object oriented programming (OOP), through the introduction of classes.

These extensions to Modula-2 should not, of course, be used if program portability to other environments is desired.

14.1 Highlights of the implementation

Readers already familiar with OOP terminology will be keen to note the highlights of this implementation. The FST extensions to Modula-2 are a small, but well thought out, selection of the many ideas that have been put forward in recent years:

OOP features are supported by the use of five new reserved words (CLASS, INHERIT, OVERRIDE, INIT and DESTROY), three new standard identifiers (SELF, SUPER and MEMBER), and the Objects library module.

Class definitions may appear in definition modules, leaving the details of the implementations to implementation modules.

Class implementations may extend the class definition, adding new attributes and methods to the original definition.

Only single inheritance is allowed in this release.

Methods declared in a class definition are virtual.

Methods declared in a class implementation are static.

All objects are dynamic, i.e. you must create all objects before using them. There is no automatic garbage collection, i.e. you must explicitly dispose of objects when no longer required. Effectively, classes are a special kind of pointer type, but that fact remains largely hidden from the user.

Class implementations may define object constructor (INIT) and destructor (DESTROY) methods that are invoked automatically when an object is created (by NEW) or destroyed (by DISPOSE).

Local classes may be implemented within program and implementation modules. In such cases, the class implementation is also its definition (in an analogous manner to local modules not requiring a separate definition).

Classes cannot, however, be implemented inside local (nested) modules.

14.2 Classes, attributes, methods, inheritance and polymorphism

This section aims to give a very brief introduction to the terminology and ideas of object oriented programming (OOP) for new readers.

Support for an "object" oriented paradigm of programming in Modula-2 at the lowest level consists of allowing you to define object "types" (record, arrays, ...) and to write procedures that manipulate parameters of those classes of objects. Thus we might have:

             Gender = (male,female);
             Person = RECORD
                        name : ARRAY [0..40] OF CHAR;
                        sex  : Gender;

            PROCEDURE IsMale (Human : Person) : BOOLEAN;
               RETURN Human.sex = male
             END IsMale;
In Modula-2 in particular, it is common to find all these related definitions encapsulated in a module. Indeed, one can go further and hide the details of the type within the implementation; the library module Windows, for example, defines an opaque type Window and a bunch of procedures that operate on objects of that type (OpenWindow, SelectWindow, ... ).

Once one has defined a type like Person, one can go on to use it in the definition of other types

             Programmer = RECORD
                who : Person;
                favoriteLanguage : ARRAY [0..10] OF CHAR;
and introduce other procedures to manipulate parameters of the Programmer type. But the awkward fact remains that, while in real life programmers are also persons, in Modula-2 variables of Programmer type are inherently incompatible with variables of Person type. Furthermore, if one wishes to hide the details of the types by defining them as opaque types, the mechanisms allowed depend heavily on awkward pointer manipulations.

Thus, it can be said that the methods traditionally used in Modula-2 for program decomposition, while reducing the complexity of a total system, often tend to produce building blocks that are specific to the program at hand, and are reusable for other purposes only after a lot of extra effort has been put into modifying them. Re-using components often requires many textual changes, which can lead to an entire family of "similar" components that must all be managed and maintained.

Enter the new OOP ideas! Better support for the paradigm is realized by extending the idea of a RECORD type to the idea of a CLASS type.

A Class is a new user defined type. A class defines both the "attributes" (fields) and "methods" (procedures) of the "instances" (objects) of that class; a CLASS declaration combines the declaration of an object's structure with the declaration of all the procedures (methods, in OOP terminology) that deal with that kind of object. An OOPed version of FileSystem might be introduced along these lines:

            DEFINITION MODULE OOPFileSystem;
                Response = ( done, notdone );
                IOMode   = ( read, write, io );
              CLASS File;
                id   :INTEGER;
                res  :Response;
                eof  :BOOLEAN;
                mode :IOMode;
                PROCEDURE Lookup(
                    filename :ARRAY OF CHAR; new :BOOLEAN
                PROCEDURE ReadWord( VAR w :WORD );
                PROCEDURE WriteWord( w :WORD );
              END File;
            END OOPFileSystem.
This code, actually, gives the class definition rather than the class implementation -- the implementation would be elaborated in a corresponding implementation module. As usual, the definition gives all we need to know to be able to start using the class -- a program using OOPFileSystem might be coded like:

            FROM OOPFileSystem IMPORT File;
                f :File;
                w :WORD;
                NEW(f); (* create an object of the File class *)
                f.Lookup( "myfile.txt", FALSE ); (* open file *)
                IF f.res = done THEN
                    f.ReadWord( w );

We have coded f.ReadWord(w) instead of ReadWord(f,w). This is called "sending a message to the object". What used to be a parameter (f) now becomes the recipient of the message -- we send the message Lookup to the object f, which is "intelligent" enough to know how to react to the message and do something to itself.

We had to go further than just declare an instance f of the class File; we had to create the instance by calling NEW(f), in an analogous way to that used for creating dynamic variables. This is a feature of the FST dialect -- other languages do this creation automatically once the instances (variables) are declared.

In FileSystem.File there is a field (fdptr) of an opaque type, which is used to hold implementation specific information. This is not needed in OOPFileSystem.File because, as we shall see, we can add the needed fields to File in its implementation.

So far this does not seem to have bought us much other than a syntactic change, writing code like f.ReadWord(w) in place of ReadWord(f, w). However, a very real advantage comes from being able to extend classes. Consider a class definition for a Person

            TYPE Gender = (unknown,male,female);
            CLASS Person;
              name    :ARRAY [0..40] OF CHAR;
              sex     :Gender;
              PROCEDURE isMale() :BOOLEAN;
            END Person;
Notice that the method takes no parameter (as we have implied, the method will "know" about the object already), in distinction to the style used in writing traditional procedures:

            IF Queen.isMale() THEN SomethingWrong END;
Programmers are also people, but they have further attributes and methods for dealing with them:

            TYPE LanguageName = ARRAY [0..10] OF CHAR;
            CLASS Programmer;
              INHERIT Person;
              favoriteLanguage :LanguageName;
              PROCEDURE isSmart() :BOOLEAN;
            END Programmer;

            VAR Hacker  :Programmer;
Note the INHERIT clause: an object of the Programmer class is going to have all the attributes of a Person, and be subjectable to the methods that can be applied to a Person -- as well as having new properties of its own. The Programmer class is said to be based on the Person class, or to be a "sub-class" or descendant of the Person "super-class" or ancestor.

            IF Hacker.isSmart() THEN
                IF Hacker.isMale() THEN Employ(Hacker)
                ELSE Exploit(Hacker)
This implies that an object of the Programmer class is compatible, to some degree, with an object of a Person class -- even though it appears to be "bigger"! The procedures (or methods) like Employ and Exploit might take parameters of the Programmer class; they might equally well take parameters of the Person class.

This guaranteed compatibility of derived data types with their base types lies behind the realization of an idea known as polymorphism. Some variables must be able to assume values of different (but related) data types at run time and, in the course of operations with objects, there must be the possibility of determining at run time the concrete actions to be executed (depending on the current dynamic data type of the object).

All of this represents quite a departure from the very strict typing philosophy of Modula-2.

As we have already noted, activating an operation on an object is often termed "sending a message to the object". The object reacts by executing a method. The assignment of methods to messages is determined for each class by the respective class definition, and the effect of sending a message differs from procedure invocations in conventional programming languages in that the determination of which method is to be executed can (and often must) occur at run time, rather than at compile time. This is discussed further in the section "Data Structures with Classes".

14.3 Defining classes in definition modules

Time for some specifics that apply to this compiler.

A Class definition may appear in a definition module, where it specifies the format of the data to be allocated when an object of that class is instantiated, and a set of methods (procedures) that are available to manipulate that object.

A class definition is analogous to a type definition; the extended syntax for definitions in a definition module is described by
            definition = "CONST" {ConstantDeclaration ";"} |
                         "TYPE" {ident ["=" type] ";"} |
                         "VAR" {VariableDeclaration ";"} |
                         "CLASS" ClassDefinition ";" |
                         ProcedureHeading ";" .

            ClassDefinition =
                    className ";"
                    [ "INHERIT" someClassName [ "OVERRIDE" IdentList ] ";" ]
                    { AttributeDeclaration }
                    { MethodDefinition }
                    "END" className .

            AttributeDeclaration =  VariableDeclaration ";"  .

            MethodDefinition = ProcedureHeading .
            className = ident .
            someClassName = qualident .
An example of a Definition Module incorporating CLASS definitions is:

            DEFINITION MODULE People;
              TYPE Gender = (unknown,male,female);
              CLASS Person;
                name    :ARRAY [0..40] OF CHAR;
                sex     :Gender;
                PROCEDURE isMale() :BOOLEAN;
              END Person;

              TYPE  LanguageName = ARRAY [0..10] OF CHAR;
              CLASS Programmer;
                INHERIT Person;
                favoriteLanguage :LanguageName;
                PROCEDURE isSmart() :BOOLEAN;
              END Programmer;
            END People.

The attributes are effectively the data fields of the class (object).

Methods are effectively procedures and, therefore, defined as such in an analogous way to defining other procedures exported from the module.

The methods are technically known as "virtual methods". An implementation of each one must be declared in the corresponding implementation module; however, this implementation may be overridden in classes derived from the one being defined.

A class is really a kind of pointer, so an attribute of a class may be defined in terms of the class itself. This is useful in setting up linked structures (For examples of this, please refer to the section "Data Structures with Classes" and the Lists example supplied with the compiler).

FST Modula-2 only supports single inheritance -- a subclass can only inherit directly from its one immediate superclass.

14.4 Implementing classes in implementation modules

A class defined in a definition module must, of course, be elaborated (its methods fully declared) in the corresponding implementation module. An implementation module or program module may also declare local classes.

The syntax of declarations in this extension of Modula-2 is described by

            declaration =
                CONST {ConstantDeclaration ";"} |
                TYPE {TypeDeclaration ";"} |
                VAR {VariableDeclaration ";"} |
                "CLASS" [ ClassImplementation | ClassDeclaration ] ";" |
                ProcedureDeclaration ";" |
                ModuleDeclaration ";".

            ClassImplementation =  (* in implementation modules *)
                className ";"
                { AttributeDeclaration }
                { MethodImplementation }
                [ "INIT" StatementSequence ]
                [ "DESTROY" StatementSequence ]
                "END" className .

            ClassDeclaration = (* used for local classes *)
                className ";"
                [ "INHERIT" someClassName [ "OVERRIDE" IdentList ] ";" ]
                { AttributeDeclaration }
                { MethodImplementation }
                [ "INIT" StatementSequence ]
                [ "DESTROY" StatementSequence ]
                "END" className .

            MethodImplementation = ProcedureDeclaration .
Notice that a ClassDeclaration for a local class may inherit from another class; however, in a ClassImplementation that completes the elaboration of a class defined in a definition module, the INHERIT clause must not be repeated.

An example of an Implementation Module incorporating CLASS definitions is:

              FROM Strings IMPORT CompareStr, Assign;

              CLASS Person;    (* a class implementation *)
                PROCEDURE isMale() :BOOLEAN;
                    RETURN sex = male;
                  END isMale;
                  name := "";
                  sex := unknown;
             END Person;

              CLASS Programmer;  (* a class implementation *)
                PROCEDURE isSmart() :BOOLEAN;
                    RETURN CompareStr(favoriteLanguage,"Modula-2") = 0;
                  END isSmart;
                  favoriteLanguage := "?";
              END Programmer;

              CLASS Vendor;      (* a local class declaration *)
                INHERIT Programmer;
                BusinessAddress : ARRAY [0..40] OF CHAR;
                PROCEDURE GetAddress (VAR Address : ARRAY OF CHAR);
                    Assign(BusinessAddress, Address)
                  END GetAddress;
                  BusinessAddress := "PO Box 867403, Plano, Texas"
              END Vendor;
            END People.

Local classes may be declared in program modules or implementation modules only at the outermost level, i.e. they may not be declared in local (nested) modules.

In a class implementation, further attributes may be added to those already defined in the class definition as it appears in the definition module.

In a class implementation, further methods may be added to those introduced in the class definition. These are known as "static" methods.

Direct access to the attributes exported from the class definition is allowed, but should be avoided -- rather provide methods for accessing the attributes safely.

Inside a method, you have full access (with no need for qualification) to the fields of the object that you are dealing with.

When a method is called, the reference to the object on behalf of which the method will operate forms part of the message. If you need it, you may access this object's reference through the predefined object handle SELF.

14.5 SELF

SELF is an identifier, pre-declared in every method, that is associated with the object instance on behalf of whiuch the method was invoked (the object that the message was sent to).

In isSmart, above, we could have used SELF

CompareStr(SELF.favoriteLanguage,"Modula-2") = 0;

but that is not necessary in this case.

You would, typically, use SELF when you want to pass your object's reference as a parameter to a method of another class. We show an example of this in our Lists example.

14.6 SUPER
SUPER, like SELF, is pre-declared in every method. SUPER refers to the parent of the class that the method is defined in, and is used to invoke a method that is overriden in the current class.

SUPER is useful in cases where you do not really want to override a method of a parent class but you want to extend it instead. You use it something like:

            CLASS Child;

                INHERIT Parent OVERRIDE printSelf;

                PROCEDURE printSelf;
                    SUPER.printSelf;    (* print parent's stuff *)
                    (* print my extensions *)
                END printSelf;

            END Child;

14.7 Object instantiation, creation and destruction

An object handle holds a reference (call it a pointer if you must) to an object instance.

You declare an object handle just as you would declare a variable, using for its type the name of the class (of objects) that this handle will refer to. For example:

VAR myBoss :Person;

However, since in this implementation all objects are "dynamic", before you can use an object you must instantiate (create) it via a call to the standard procedure NEW, passing NEW the handle as a parameter (This is somewhat analogous to using NEW to allocate storage for dynamically created variables accessed via other pointers in Modula-2). For example:

NEW( myBoss );

NEW invokes the procedure ALLOCATEOBJECT, which allocates the necessary amount of storage to hold the object's data and then invokes the object's initialization code defined for the class. This initialization code is defined by the statement sequence following INIT in the class implementation.

Once you no longer need the object, you may get rid of it via the standard procedure DISPOSE. For example:

DISPOSE( myBoss );

DISPOSE invokes the procedure DEALLOCATEOBJECT, which deallocates the necessary amount of storage to hold the object's data after invoking the object's destruction code defined for the class. This destruction code is defined by the statement sequence following DESTROY in the class implementation.

Implementations of the procedures ALLOCATEOBJECT and DEALLOCATEOBJECT are found in the library module Objects (or you can write your own). Typically, an OOP module imports from Objects, of course.

It may be desirable to have multiple object handles reference the same object. We can easily do this by assigning the value of an object handle to another object handle.

objHandle2 := objHandle1;

Please note that we are assigning object handles, not making a copy of the object itself. If we DISPOSE(objHandle1)', objHandle2 will hold an obsolete handle. This is analogous to the "dangling pointer" situation that can arise with other dynamically allocated variables accessed through pointers in the familiar way.

14.8 Object compatibility

The possibility of assigning one object handle to another, or of passing objects as parameters to procedures or methods, brings us to the question of compatibility rules between objects.
14.8.1 Assignment compatibility
In FST Modula-2 two object handles are assignment compatible if they are of the same class type, or if the type of the source operand is a subclass (descendant) of the type of the destination operand. For example, consider the declarations

                myBoss :Person;
                me     :Programmer (* inherits from Person *);
me is assignment compatible with myBoss, but not the other way around:
            myBoss := me;       (* right! *)
            me := myBoss;       (* wrong *)
The simple way to remember this is that an assignment is legal if the source can completely fill the destination (the fields that are left over are just "truncated"), but not the reverse (as that would leave fields dangerously undefined).

In point of fact, classes in FST Modula-2 are really clever pointers to record types, rather than clever record types (as they are in Oberon). So, the assignment is really of one machine address to a destination, rather than copying a collection of field values, and the above explanation is not quite honest (In other OOP languages, the distinctions between classes and subclasses and between pointers to classes and pointers to subclasses are much more complex.).
14.8.2 Expression compatibility
In FST Modula-2, the only operators that may appear between two object operands are the Boolean test for equality and for inequality. There is an asymmetry, however. Given the declarations above, we may write code like
            IF myBoss = me THEN Rejoice END;
            IF (you = me) OR (me = you) THEN SomethingFunny END;
but not
            IF me = myBoss THEN WeShallNeverKnow END  (* illegal *);
the type of the right operand may be a subclass of the type of the left operand, but not vice versa. This makes sense -- since one could never have made an assignment like me := myBoss there is no way that me could become equal to myBoss anyway. But given that FST objects are really implemented as pointers to super records, such comparisons are misleading in any case!
14.8.3 Parameter compatibility
Compatibility is needed between the formal and actual parameters of a method or procedure.

In FST Modula-2, an object handle used as an actual parameter, is compatible with a formal parameter, passed either by value or by reference, if the formal and actual parameters are of the same class type, or the class type of the actual parameter is a subclass (descendant) of the class of the formal parameter.

This is effectively the same rule as for assignment compatibility.

Since we usually have methods incorporated into classes, this still makes sense. For example, the class Person has methods that know how to deal with objects of that class. The subclass Programmer adds methods that know how to deal with the Programmer extensions to Person. If one were to apply a Programmer method to a simple Person, the method would very likely fail because it tried to access non-existent fields of the Person structure (for example, isSmart needs access to the favoriteLanguage attribute). But a Person method (like isMale) would have no trouble handling a Programmer, whose structure includes all the Person information.

Do not forget that the method Person.isMale is also a method of the class Programmer!

14.9 The MEMBER function

Clearly situations will arise where a procedure or method needs to know exactly what type its actual parameter is, since the actual parameter may be of a type that is a subclass of the formal parameter.

Hmmm. That's a nice point. Formal parameters of dynamically varying types? Well, yes, you can almost think of it like that!

FST Modula-2 has a standard function called MEMBER:

MEMBER( objectHandle, className )


if Object is of the class denoted by ClassName or

if ClassName is a superclass (ancestor) of the actual class of Object.

To use the function MEMBER, you must import MEMBEROBJECT from the library Objects (or write your own version of MEMBEROBJECT).

14.10 Compatibility between objects and other types

Since in FST Modula-2 classes are, really, disguised pointers, object handles are compatible with NIL and with SYSTEM.ADDRESS. However, direct use of this last feature should probably be minimized.

14.11 Pointers to classes

The fact that objects are really a disguised form of pointer is very useful when we want to try to create base classes that handle linked structures (because attributes of a class may be defined as being of the class type itself, just as nodes pointed to in a traditional linked structure may have fields that point to other nodes in the same way).

Of course this does not prevent us from making declarations like

            CLASS BaseClass;
                x, y :AttributeType;
            END BaseClass;

            CLASS SubClass;
                INHERIT BaseClass;
                a, b :AnotherAttributeType;
            END SubClass;

                BasePtr = POINTER TO BaseClass;
                SubPtr  = POINTER TO SubClass;
                    B  :BasePtr;
                    S  :SubPtr;
In other OOP languages this may be necessary for the creation of linked structures. In such languages (but not in FST Modula-2) SubPtr is then usually taken to be an extended pointer type of BasePtr, just as SubClass is an extension of BaseClass, and then special compatibility rules might exist between variables and parameters of these two types;

for example we might try to write code like

            B := S   (* legal, but not in FST *);
            S := B   (* always illegal *);
and we might be able to pass S by value (but not by reference, that is as a VAR parameter) to a procedure whose corresponding formal parameter is of type BasePtr.

14.12 Complete example

Here is a simple example to illustrate the ideas of the last sections: using the classes declared in People.

            MODULE UsePeople;

            FROM InOut   IMPORT WriteString, WriteLn;
            FROM People  IMPORT Person, Programmer, Gender;

             someone         :Person;
             smartProgrammer :Programmer;

            PROCEDURE DisplaySkills ( VAR Student :Person );
              P  :Programmer;

              IF Student.isMale()
                THEN WriteString( "He " )
                ELSE WriteString( "She " )
              IF MEMBER( Student, Programmer )
                  WriteString( "is a programmer " );
                  P := Programmer( Student );
                  IF P.isSmart()
                    THEN WriteString( "and has learned a fine language" )
                    ELSE WriteString( "but never learned Modula-2" )
                ELSE WriteString( "has managed to avoid programming ")
            END DisplaySkills;

             NEW( smartProgrammer );
             smartProgrammer.sex := female;
             DisplaySkills( smartProgrammer );

             NEW( someone );
             someone.sex := male;
             DisplaySkills( someone );

             smartProgrammer.favoriteLanguage := "Modula-2";
             DisplaySkills( smartProgrammer );

             someone := smartProgrammer;
             IF MEMBER( someone, Programmer ) & smartProgrammer.isSmart()
                 WriteString( "What a smart programmer to use Modula-2!" );
            END UsePeople.
Within the DisplaySkills procedure we may apply the isMale method regardless of the apparent type of the formal parameter. We may apply the isSmart method only to objects known to be of the Programmer subclass of Person. Such an object might be passed as an actual parameter to Student, so the type test

IF MEMBER( Student, Programmer )

is used to decide on the action. Notice, however, that we have to make a typecast involving the formal parameter Student to the local variable P, because we are not in a position to apply the isSmart method to an object of type Person directly (even though we know that at run-time things will be all right, at compile-time the compiler is trying to catch all type-breaking that it can). This type cast is quite safe, because all objects are really pointers.

Notice, by contrast, how the MEMBER function is applied without subsequent typecasting in the main program in the line

IF MEMBER( someone, Programmer ) & smartProgrammer.isSmart() THEN

The output from this program would be

She is a programmer but never learned Modula-2
He has managed to avoid programming
She is a programmer and has learned a fine language
What a smart programmer to use Modula-2!

(A programmer coming up with code like that above would be justifiably proud of the definition of a smart programmer. However, the programmer's employer, who probably is one of the great unwashed who do not know Modula-2, might not have liked it. After all, how smart a programmer is has more to do with the tools that he or she chooses and how the tools are used, rather than simply relying on the knowledge of a favourite language! Hmmm...)

14.13 Virtual methods

Earlier we mentioned that, in FST Modula-2, methods defined in a definition module are technically known as virtual methods. What exactly does this mean, and how do virtual methods differ from another kind of method, known as a static method?

Methods defined in a definition module, just like the familiar procedures defined in a definition module, need to be "elaborated", or "declared" in full, in the corresponding implementation module. We saw an example of this in the People implementation module, where the method isMale (for the Person class) is implemented as a simple predicate defined on the Gender attribute of a person, and the method isSmart (for the Programmer class) is implemented as a simple predicate defined on one's preference for Modula-2.

But suppose we define a further class that inherits from Programmer. To satisfy the boss, suppose we come up with this new class:
            DEFINITION MODULE MyKindOfProgrammers;
            FROM People IMPORT Programmer, LanguageName;

            CLASS Modula2Programmer;
              INHERIT Programmer OVERRIDE isSmart;
              compilerVendor :ARRAY [0..2] OF CHAR;
            END Modula2Programmer;

            END MyKindOfProgrammers.
The new class Modula2Programmer now has attributes

name, sex (inherited via Programmer from Person)
favoriteLanguage (inherited from Programmer)
compilerVendor (defined for itself)

and it has methods

isMale (inherited via Programmer from Person)
isSmart (inherited from Programmer)

So far, so good. But the implementation of Modula2Programmer might wish to use a different predicate for "smartness" -- perhaps, for this class of programmer, the smart ones are those that use FST Modula-2?

We are allowed to override the implementation of an otherwise inherited method by stating our intention, via the OVERRIDE clause in the INHERIT statement (as above), and redeclaring the new method in the implementation of the subclass (in this example, Modula2Programmer); for example:
            IMPLEMENTATION MODULE MyKindOfProgrammers;
            FROM Strings IMPORT CompareStr;
            CLASS Modula2Programmer;
              PROCEDURE isSmart() :BOOLEAN;
                  RETURN CompareStr(compilerVendor,"FST") = 0;
                END isSmart;
                compilerVendor := "FST";
            END Modula2Programmer;

            END MyKindOfProgrammers.
What about the boss?! Well, he can create his own programmer classes if he wants to!

What have we done? Let's see how these classes could be used:
            MODULE x;
            FROM InOut IMPORT WriteString, WriteLn;
            FROM People IMPORT Programmer;
            FROM MyKindOfProgrammers IMPORT Modula2Programmer;
            FROM Objects IMPORT

            CLASS CProgrammer;
              INHERIT Programmer;
            END CProgrammer;

            VAR m2programmer :Modula2Programmer;
                cprogrammer  :CProgrammer;

              NEW( m2programmer );
              NEW( cprogrammer );
              IF m2programmer.isSmart() THEN
                WriteString( "this m2 programmer is smart" ); WriteLn;
              IF cprogrammer.isSmart() THEN
                WriteString( "this C programmer is smart" ); WriteLn;
            END x.
This program would output "this m2 programmer is smart".

In the implementation of Modula2Programmer we have effectively redefined the method isSmart, and so the message m2programmer.isSmart invokes this new method, because m2programmer is of this class. But since CProgrammer did not redefine isSmart, the message cprogrammer.isSmart invokes the method implemented in Programmer, and inherited by CProgrammer. Makes sense, does it not?

Now let us consider the following (similar) example:
            MODULE y;
            FROM InOut IMPORT WriteString, WriteLn;
            FROM People IMPORT Programmer;
            FROM MyKindOfProgrammers IMPORT Modula2Programmer;

            CLASS CProgrammer;
              INHERIT Programmer;
            END CProgrammer;

            VAR m2programmer :Modula2Programmer;
                cprogrammer  :CProgrammer;

            PROCEDURE ifIsSmart( p :Programmer; s :ARRAY OF CHAR );
                IF p.isSmart() THEN
                  WriteString( s ); WriteLn;
              END ifIsSmart;

              NEW( m2programmer );
              NEW( cprogrammer );
              ifIsSmart( m2programmer, "1st m2 programmer is smart" );
              ifIsSmart( cprogrammer, "1st C programmer is smart" );
              m2programmer.favoriteLanguage := "C";
              cprogrammer.favoriteLanguage := "Modula-2";
              ifIsSmart( m2programmer, "2nd m2 programmer is smart" );
              ifIsSmart( cprogrammer, "2nd C programmer is smart" );
            END y.
In this case we would get the following result:

1st m2 programmer is smart
2nd m2 programmer is smart
2nd C programmer is smart

This may not be quite as obvious, although it certainly is what we would have wanted: Modula2Programmer's are smart, regardless of their favorite language, if they use FST Modula-2 -- their implementation os isSmart depends on their vendor, not on any fickle philandering with other languages.

By default, Programmers are smart if their favourite language is Modula-2. So, since a CProgrammer inherits the default method, all we have to do is convince the CProgrammer that his favourite language should change (to Modula-2) for the default method to be able to react properly.

How is it that the ifIsSmart method is invoking different procedures at different times, when the programmed call is, obviously, the same? Why does the code in ifIsSmart not do what it appears to do, namely call a method declared when we implemented the Programmer class -- after all, the formal parameter p is apparently declared of the Programmer class.

We have a feature that is known as dynamic binding. Since the method (procedure isSmart) is virtual, the actual procedure invoked by p.isSmart in ifIsSmart is not determined until run time, when it depends on the actual run time type of the object referenced by p (remember that the actual parameter passed to p can be of class Programmer or any subclass of Programmer).

Note, incidentally, that

MEMBER( cprogrammer, Modula2Programmer )

would have returned FALSE, but

MEMBER( cprogrammer, Person )

would have returned TRUE. We have already implied that objects have to be able somehow to "remember" the actual type of object that has been assigned to them, since this can be the class itself, or a subclass. So, classes have rather more "intelligence" than the old style records you have used in the past.

The form of binding of procedure calls that you have become familiar with in standard Modula-2 is known as "static binding" -- when a compiler sees a procedure call, it normally knows at compile time which procedure will actually be invoked at run time (the only way it cannot know is if the procedure call is via a "procedure variable"). Hopefully, you can see that this is more "efficient" than delaying the decision till run time. Hopefully, you can also see that being able to delay the decision opens the door to a whole new approach to programming.

For those familiar with other OOP languages we point out that, in some applications of OOP techniques, the programmer may be able to foresee that the ability to override a method may never be needed, and that the inefficiency of delaying a decision until run time that really can be made at compile time is simply unwarranted. So, some OOP languages (but not FST) allow one to declare methods (at the definition module level) as either VIRTUAL (that is, to allow overriding) or STATIC (not allow it). The distinction is usually drawn by using a reserved word like VIRTUAL where needed, and omitting it where "static" is implied (just as when we omit VAR to get parameters passed by value by default).

For simplicity, FST Modula-2 has the following rules:

methods defined in a definition module are all virtual

methods defined in a definition module may be overridden in a subclass by methods that must, however, be of the same procedure type as the original method (some other languages relax this rule)

further local methods, declared in an implementation or program module, are static and cannot be overriden in descendant classes.

This means that the following code is not allowed.

            MODULE SexualHarassment;

             Gender = (unknown, male, female);

            CLASS Person;    (* a class implementation *)
             sex :Gender;

             PROCEDURE isEligible () :BOOLEAN;  (* a static method *)
                 RETURN sex = male;
               END isEligible;

               sex := unknown;
             END Person;

            CLASS Woman;     (* a class implementation *)
             INHERIT Person; (* women are persons too *)

             PROCEDURE isEligible () :BOOLEAN
             (* illegal - cannot override a static method *);
                 RETURN sex = female;
               END isEligible;

               sex := female;
             END Woman;

            END SexualHarassment (* and not before time either *).
Chauvinist system. To be eligible one has to be male. Oh well!

14.14 Data Structures with Classes

We have already hinted on several occasions that, in FST Modula-2, classes are really clever pointers to clever records. This means that setting up dynamic lists, trees and the like is very easy. It also means that we at last have the possibility of setting up truly extensible and "generic" base classes for nodes in such structures, from which special purpose variations may easily be crafted without the need for editing or recompiling the base classes.

To illustrate this, let us consider the workhorse data structure, the stack. A stack is built of a sequence of nodes, and an implementation of a stack ADT entails operations to push and pop nodes at least, plus a few other expedient access procedures. Consider the following very rudimentary definition:
            DEFINITION MODULE Stacks;

             (* Base types for NODES to be stored in STACKS *)

               CLASS NODE;
                 (* We hide the structure of a NODE in the implementation,
                    as the user has no need to access that information *)
               END NODE;

               CLASS STACK;

                 PROCEDURE Push ( N :NODE );

                 PROCEDURE Pop ( VAR N :NODE );

                 PROCEDURE IsEmpty () : BOOLEAN;

                   When this stack is DISPOSEd, all the nodes in the
                   stack are DISPOSEd too.

               END STACK;

            END Stacks.
Here the NODE class seems to be utterly "useless", for it seems to have no attributes (either declared, or made accessible through methods) in which we could store any "real" data. Hopefully the reader can foresee that we shall be able to extend this class in client programs to add such data attributes. The STACK class, similarly, has no visible attributes, though it does have methods for pushing and popping our base class nodes, and for testing for an empty stack.

Implementation of this pair of classes is straightforward:


               CLASS NODE;   (* IMPLEMENTATION *)

                 Next :NODE; (* effectively a pointer to next node
                                in the stack *)

                 INIT        (* Next = NIL means that we are not
                                in any list *)
                   Next := NIL;

               END NODE;

               CLASS STACK;  (* IMPLEMENTATION *)

                 Top :NODE;   (* Hidden - Top node on the stack *)

                 PROCEDURE Push ( N :NODE );
                     N.Next := Top;
                     Top := N;
                   END Push;

                 PROCEDURE Pop ( VAR N :NODE );
                     N := Top;
                     Top := N.Next;
                     N.Next := NIL (* N is no longer in any stack *)
                   END Pop;

                 PROCEDURE IsEmpty () : BOOLEAN;
                     RETURN Top = NIL
                   END IsEmpty;

                 PROCEDURE DestroyStack;
                 (* DestroyStack is coded as a separate static method
                    instead of doing the work directly in DESTROY,
                    because it requires a work node *)
                     N :NODE;
                     WHILE NOT IsEmpty() DO
                       Pop( N );
                       DISPOSE( N );
                   END DestroyStack;

                   Top := NIL   (* initialize all our attributes *);


               END STACK;

            END Stacks.
The code should have a familiar feel, and yet look slightly strange at first -- the familiar dereferencing operators (^) have all gone away.

When we create a stack, say S, it is as though we create a "header node" with a Top field, and set it to NIL. But rather than having to code a procedure like
            PROCEDURE IsEmpty ( S :STACKTYPE ) :BOOLEAN;
                RETURN S^.Top = NIL
            END IsEmpty;
we simply have to code a corresponding method
            PROCEDURE IsEmpty () :BOOLEAN;
                RETURN Top = NIL
            END IsEmpty;
Remember that the method "knows" about the implicit parameter -- recall that we invoke the method with a "message" like S.IsEmpty() rather than calling the procedure IsEmpty(S).

The only subtle point to which we should draw attention is that, since a NODE object includes a (hidden) link to other such objects, we shall never be able to store a given object in more than one stack at a time. This means that we must maintain the integrity of the Next "pointer" for each node rather systematically.

A very simple program that uses this base class by extending it locally, so as to provide nodes that have an INTEGER item field might be as follows:
            MODULE UseStack;

              FROM Stacks  IMPORT NODE, STACK;
              FROM InOut   IMPORT WriteInt;

              CLASS INTNODE;
                INHERIT NODE;
                Item :INTEGER;
              END INTNODE;

                S  :STACK;
                I  :INTNODE;
                J  :CARDINAL;

                NEW( S )                ( * instantiate the stack * );
                FOR J := 1 TO 5 DO
                  NEW( I ); I.Item := J ( * instantiate an integer node * );
                  S.Push( I )           ( * and push it onto the stack * );
                FOR J := 1 TO 5 DO
                  S.Pop( I )            ( * pop an integer node * );
                  WriteInt( I.Item, 0 ) ( * and display its value * );
            END UseStack.
For a rather larger example of the use of classes as they apply to data structures, please look at the modules Lists, MyLists and Uselists supplied with the implementation.

14.15 The technical details

When we declare a CLASS, the compiler generates a class template. The class template is a structure (the header of which is defined in Objects.DEF and shown below) that contains a reference to the parent class, the size of objects of this class, and a pointer to each method in this class.

            ClassHeader = RECORD
              parent      :Class;
              size        :CARDINAL;
              filler      :CARDINAL;
              InitProc    :PROCEDURE( ADDRESS );
              DestroyProc :PROCEDURE( ADDRESS );
              ... other virtual methods ...
When an object is created, the ALLOCATEOBJECT procedure allocates enough space to hold the data in the object's structure plus a pointer to the object's class. So, as you can see, regardless of the declared type of the object handle that refers to a particular object, we can, by looking at the object itself, determine what its real type is.

As you might have guessed by now, we invoke a method by picking up its address from the class template referenced by the object itself.

There is only one exception to this. As was stated earlier, methods declared in implementation modules are static. These methods cannot be redefined and, therefore, are not placed in the class template.

In summary:

A virtual method may be redefined and its invocation is dynamically resolved at run time.

A static method cannot be redefined and its invocation is resolved at compile time.

All methods defined in a definition module are virtual. All methods first declared in a program or implementation module are static.

It may also help to give a more insightful explanation of the methods INIT and DESTROY.

We have seen that the methods defined in INIT and DESTROY are automatically invoked when an object is created or destroyed.

When the compiler sees a NEW(objectHandle), it generates code that invokes the procedure ALLOCATEOBJECT, which must be known in the current environment -- you must either import this procedure from the standard module Objects or write your own.

ALLOCATEOBJECT takes two parameters: a VAR object handle and a class identifier (pointer to the class template).

First, ALLOCATEOBJECT calls Storage.ALLOCATE to allocate memory for the object and stores the address of the class template in the first double word of the object just allocated.

Next, and starting with the base class in the hierarchy, ALLOCATEOBJECT invokes all the INIT methods that were defined along the way. In the previous example, the call NEW(m2programmer) caused person.INIT, Programmer.INIT and Modula2Programmer.INIT to be invoked, in that sequence.

For DISPOSE(objectHandle), the compiler generates a call to DEALLOCATEOBJECT, also defined in Objects.

DEALLOCATEOBJECT works in the reverse order of ALLOCATEOBJECT. It invokes all the DESTROY methods defined in the class hierarchy, starting with the class of the current object. It then invokes Storage.DEALLOCATE to free the object's memory.

14.16 On your own...

This concludes our introduction to object oriented programming using FST Modula-2.