Modula-3: Object types

# Object types

An object is either `NIL` or a reference to a data record paired with a method suite, which is a record of procedures that will accept the object as a first argument.

An object type determines the types of a prefix of the fields of the data record, as if "`OBJECT`" were "`REF RECORD`". But in the case of an object type, the data record can contain additional fields introduced by subtypes of the object type. Similarly, the object type determines a prefix of the method suite, but the suite can contain additional methods introduced by subtypes.

If `o` is an object, then `o.f` designates the data field named `f` in `o`'s data record. If `m` is one of `o`'s methods, an invocation of the form `o.m( ... )` denotes an execution of `o`'s `m` method. An object's methods can be invoked, but not read or written.

If `T` is an object type and `m` is the name of one of `T`'s methods, then `T.m` denotes `T`'s `m` method. This notation makes it convenient for a subtype method to invoke the corresponding method of one of its supertypes.

A field or method in a subtype masks any field or method with the same name in the supertype. To access such a masked field, use `NARROW` to view the subtype variable as a member of the supertype, as illustrated below.

Object assignment is reference assignment. Objects cannot be dereferenced, since the static type of an object variable does not determine the type of its data record. To copy the data record of one object into another, the fields must be assigned individually.

There are two predeclared object types:

        ROOT           The traced object type with no fields or methods
        UNTRACED ROOT  The untraced object type with no fields or methods

The declaration of an object type has the form:

        TYPE T = ST OBJECT
                   Fields
                 METHODS
                   Methods
                 OVERRIDES
                   Overrides
                 END

where `ST` is an optional supertype, `Fields` is a list of field declarations, exactly as in a record type, `Methods` is a list of method declarations and `Overrides` is a list of method overrides. The fields of `T` consist of the fields of `ST` followed by the fields declared in `Fields`. The methods of `T` consist of the methods of `ST` modified by `Overrides` and followed by the methods declared in `Methods`. `T` has the same reference class as `ST`.

The names introduced in `Fields` and `Methods` must be distinct from one another and from the names overridden in `Overrides`. If `ST` is omitted, it defaults to `ROOT`. If `ST` is untraced, then the fields must not include traced types. (This restriction is lifted in unsafe modules.) If `ST` is declared as an opaque type, the declaration of `T` is legal only in scopes where `ST`'s concrete type is known to be an object type.

The keyword `OBJECT` can optionally be preceded by "`BRANDED`" or by "`BRANDED b`", where `b` is a text constant. The meaning is the same as in non-object reference types.

A method declaration has the form:

        m sig := proc

where `m` is an identifier, `sig` is a procedure signature, and `proc` is a top-level procedure constant. It specifies that `T`'s `m` method has signature `sig` and value `proc`. If "`:= proc`" is omitted, "`:= NIL`" is assumed. If `proc` is non-nil, its first parameter must have mode `VALUE` and type some supertype of `T`, and dropping its first parameter must result in a signature that is covered by `sig`.

A method override has the form:

        m := proc

where `m` is the name of a method of the supertype `ST` and `proc` is a top-level procedure constant. It specifies that the `m` method for `T` is proc, rather than `ST.m`. If `proc` is non-nil, its first parameter must have mode `VALUE` and type some supertype of `T`, and dropping its first parameter must result in a signature that is covered by the signature of `ST`'s `m` method.

Examples. Consider the following declarations:

        TYPE
          A  = OBJECT a: INTEGER; METHODS p() END;
          AB = A OBJECT b: INTEGER END;

        PROCEDURE Pa(self: A) = ... ;
        PROCEDURE Pab(self: AB) = ... ;

The procedures `Pa` and `Pab` are candidate values for the `p` methods of objects of types `A` and `AB`. For example:

        TYPE T1 = AB OBJECT OVERRIDES p := Pab END

declares a type with an `AB` data record and a `p` method that expects an `AB`. `T1` is a valid subtype of `AB`. Similarly,

        TYPE T2 = A OBJECT OVERRIDES p := Pa END

declares a type with an `A` data record and a method that expects an `A`. `T2` is a valid subtype of `A`. A more interesting example is:

        TYPE T3 = AB OBJECT OVERRIDES p := Pa END

which declares a type with an `AB` data record and a `p` method that expects an `A`. Since every `AB` is an `A`, the method is not too choosy for the objects in which it will be placed. `T3` is a valid subtype of `AB`. In contrast,

        TYPE T4 = A OBJECT OVERRIDES p := Pab END

attempts to declare a type with an `A` data record and a method that expects an `AB`; since not every `A` is an `AB`, the method is too choosy for the objects in which it would be placed. The declaration of `T4` is a static error.

The following example illustrates the difference between declaring a new method and overriding an existing method. After the declarations

        TYPE
          A = OBJECT METHODS m() := P END;
          B = A OBJECT OVERRIDES m := Q END;
          C = A OBJECT METHODS m() := Q END;

        VAR
          a := NEW(A); b := NEW(B); c := NEW(C);

we have that

        a.m()  activates  P(a)
        b.m()  activates  Q(b)
        c.m()  activates  Q(c)

So far there is no difference between overriding and extending. But `c`'s method suite has two methods, while `b`'s has only one, as can be revealed if `b` and `c` are viewed as members of type `A`:

        NARROW(b, A).m()   activates  Q(b)
        NARROW(c, A).m()   activates  P(c)

Here `NARROW` is used to view a variable of a subtype as a value of its supertype. It is more often used for the opposite purpose, when it requires a runtime check.

The last example uses object subtyping to define reusable queues. First the interface:

        TYPE
          Queue = RECORD head, tail: QueueElem END;
          QueueElem = OBJECT link: QueueElem END;

        PROCEDURE Insert (VAR q: Queue; x: QueueElem);
        PROCEDURE Delete (VAR q: Queue): QueueElem;
        PROCEDURE Clear  (VAR q: Queue);

Then an example client:

        TYPE
          IntQueueElem = QueueElem OBJECT val: INTEGER END;
        VAR
          q: Queue;
          x: IntQueueElem;
          ...
          Clear(q);
          x := NEW(IntQueueElem, val := 6);
          Insert(q, x);
          ...
          x := Delete(q)

Passing `x` to `Insert` is safe, since every `IntQueueElem` is a `QueueElem`. Assigning the result of `Delete` to `x` cannot be guaranteed valid at compile-time, since other subtypes of `QueueElem` can be inserted into `q`, but the assignment will produce a checked runtime error if the source value is not a member of the target type. Thus `IntQueueElem` bears the same relation to `QueueElem` as `[0..9]` bears to `INTEGER`.

    Last modified on Wed Apr  3 09:39:02 PST 1996 by heydon
         modified on Thu Sep 15 10:44:33 PDT 1994 by kalsow