home *** CD-ROM | disk | FTP | other *** search
- Path: sparky!uunet!sun-barr!olivea!mintaka.lcs.mit.edu!nntp.lcs.mit.edu!andru
- From: andru@concerto.lcs.mit.edu (Andrew Myers)
- Newsgroups: comp.lang.modula3
- Subject: Constructors in Modula-3
- Message-ID: <ANDRU.92Sep14011727@concerto.lcs.mit.edu>
- Date: 14 Sep 92 06:17:27 GMT
- Sender: news@mintaka.lcs.mit.edu
- Distribution: comp
- Organization: MIT Laboratory for Computer Science
- Lines: 133
-
-
- 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
-