From: andru@concerto.lcs.mit.edu (Andrew Myers)
Newsgroups: comp.lang.modula3
Subject: Constructors in Modula-3
Date: 14 Sep 92 06:17:27 GMT
Distribution: comp
Organization: MIT Laboratory for Computer Science


I am very fond of Modula-3, but it has one lack that periodically
annoys me: it doesn't have constructors. A constructor, in C++
parlance, is a special initializer associated with a class. Objects of
that class cannot be created except by using one of the constructors of
the class. This restriction allows a class designer to ensure that
objects of the class will always be properly initialized.

By contrast, if I see the Modula-3 declaration

    TYPE T <: U;

I can write the statement

    VAR x := NEW(T);

and obtain an object that has not been initialized. Maybe it's a valid
object of that type; maybe not.

Modula-3 programmers, in my experience, handle this problem through the
use of conventions and careful documentation. Any interface that
describes a Modula-3 type should clearly state what is needed to
obtain a valid object of the type.

The most obvious and common convention is a "New" procedure. To create
an object of type Interface$T, we say

    VAR x := Interface$New(...)

where the "..." represents the arguments that are needed to initialize
an object. The approach works until you try to subclass, since the
"New" procedure of type "S" with S<:T cannot use T's New to initialize
the T part of S.

Another standard convention I've seen used is an "init" method. This
method is used in the following way:

    VAR x := NEW(T).init(...)

The "init" method is seldom overridden, and doing so is never useful.
Consider that "NEW(T).init(...)" is identical to "T.init(NEW(T), ...)"
if T reveals that it overrides the "init" method.  The fact that "init"
is made a method is a matter of convenient notation.  Though less
efficient, "NEW(T).init()" is easier to type and read than
"Interface_T$Init(NEW(T))".

Both of these variants avoid the limitations of "New", since the
subtype's "Init" procedure can be written as

PROCEDURE Init(x: S, S_args...) =
BEGIN
    Interface_T$Init(x, T_args...) (* or Interface_T.T.init(x, T_args...) *)
    (* Rest of S initialization here *)
END Init;

All of these solutions require careful programming, since an
initialization method must be sure to properly initialize all of the
fields of an object, and to properly initialize the supertype. Since
Modula-3 guarantees that each field of an object is initialized by the
NEW operator to some value of its declared type, all of these solutions
also overwrite existing values in the fields. This reinitialization is
only a potential inefficiency, since any old garbage is considered to
be a valid value of any type. If future implementations choose a more
intermediate point on the debuggability/performance curve, the
inefficiency will become real.

More importantly, ensuring that variables and values are properly
initialized is a major source of programming errors, and it is
unfortunate that Modula-3 doesn't help the programmer out more.

Here is a proposal for constructors in Modula-3, just as food for
thought:

In any module that fully reveals (i.e., contains "REVEAL T = ... ") an
object type T, a "constructor" may be declared. A constructor looks
much like a procedure, but it may only be used in the place of the NEW
operator, or within another constructor's body . Thus, to create a new
object of the type T with a constructor named "Make", we could say

    VAR x := Make(...)

Let T have a supertype named U with a constructor named Make_U and
a method named "index", fields named "a" and "b" of types "A" and "B",
and a field "i" of type "INTEGER". The constructor implementation syntax
looks like:

CONSTRUCTOR T Make(a_: A) =
VAR tmp := Make_B();
    (
	Make_U(a_, ....),
	a := a_,
	b := tmp,
	i := tmp.index()
    )

A constructor body contains a list of comma-separated items. At most
one item may be a constructor invocation for some supertype of T.  Call
this supertype "U".  All of the subtypes of U must be fully revealed in
the scope of the constructor, implying that a supertype constructor
must be used on a subtype of T's closest opaque supertype, if any.

Each of the remaining items is an assignment to a field belonging to T,
or to a local variable of the constructor.  Every field of T that is
not contained in U and does not have a default value must be assigned
to within the constructor body.

Because the object being constructed cannot be named, this
approach avoids the problem of defining the semantics of method
invocations on a partially constructed object, and the consequent
inefficiency that C++ creates during object construction.

A constructor can be declared in an interface, and the syntax is
similar to procedure-declaration syntax, e.g.:

CONSTRUCTOR T Make(a_: A);

A call to NEW is just like a constructor body: in addition to the usual
override assignments, one of the arguments to NEW(T, ...) may be a
constructor invocation. NEW may only be called on an opaque type if a
constructor is used. A call to NEW is required to initialize all of T,
just as a constructor body is.

Thus, 

    x := Make(...)

is really just shorthand for

    x := NEW(T, Make(...))

if "Make" is a constructor for "T".

Andrew
Newsgroups: comp.lang.modula3