From: msm@src.dec.com (Mark S. Manasse)
Newsgroups: comp.lang.modula3
Subject: Re: C++ Critique in comp.lang.c++
Date: 11 Apr 92 16:48:00 GMT
Article-I.D.: src.1992Apr11.094800.7106
Organization: DEC Systems Research Center


Ian's article on C++ is quite intersting.  It's a little too pure in its
faith in object-orientation for my tastes, but a lot of its critiques are
sensible.

There are several points of Modula-3 where I suspect that Ian would find 
too much similarity with C++ for his taste, and a few other places he might
be dissatisfied.  Here's my attempt to answer his objections in advance.  
For all those points in Ian's message I don't mention, I think that M3
already solves the problem.


VALUE mode parameters
----- ---- ----------

Both C++ and Modula-3 strive to be good systems programming languages, as
well as good object-oriented programming languages.  Consequently, there
are features exposed by these languages that Ian doesn't like, such as
VALUE parameter mode.  I think that both for compatability with external
libraries and operating systems, and for efficiency, VALUE mode is
essential.  I could believe that a persuasive argument could be made that
READONLY should be the default.


<* HIDES *> considered necessary
----------- ---------- ---------

Ian argues that one shouldn't be able to introduce conflicting names into
the scope of an object.  While this seems sensible, it conflicts badly with
Modula-3's strategy using partially-opaque types for data hiding: I can't
be expected (nor can the compiler check if I succeeded) to avoid
introducing fields and methods whose names conflict with names I can't see.
M3 does avoid one of the most serious pitfalls of C++: the key word
OVERRIDES is used to introduce replacement methods, while METHODS is used
to name a new method.

Two problems remain.  First, a subtype might introduce names into scope
that visibly in that type conflict with supertype names.  Such a conflict
should rarely cause a problem: programs that use the subtype presumably
want to reference the subtype fields when they name something.  However,
the addition of a field or method might inadvertantly change the meaning of
code that used to reference the supertype field; consequently, the compiler
might well issue a warning when it detected name conflicts, unless the pragma
<* HIDES *> preceded the declaration.  The introduction of a HIDES pragma
should be done cautiously, except in very stylized cases, such as init methods.

The final problem is that caused by partial revelation.  The declaration
of a name may appear innocent to the compiler; it may only be later on at a
point where the concrete type and a subtype are both visible that the
conflict is noticed.  A compiler warning would again be appropriate; a
variant of HIDES which specifies the type and name in conflict seems
appropriate.


On ABSTRACT types
-- -------- -----

In M3, because of partial revelation, it is very easy to introduce types
without values for some of the methods.  This error leads to a checked
runtime error at the point of method invocation, but that's a little later
than might be useful.  The compiler could easily provide some help here, with
a small amount of assistance from the interface designer.

In the absence of partial revelations, the compiler would be able to know
which methods were concrete and which abstract, and could warn of any call
to NEW an object with abstract methods, unless the programmer guaranteed
(say by writing <* ABSTRACT init, incr *>) that he was aware that some methods
were abstract.

With partial revelations, life is harder; separate compilation won't allow
the compiler to know whether a method is abstract or not.  If we provide
pragmas ABSTRACT and CONCRETE which apply to types which are partially-
revealed, and which optionally take a list of method names (with any unnamed
methods defaulting the other way), we can check the uses of every type, and
we can have the compiler check that the type meets its abstractness
specification.  It's probably sensible to make CONCRETE the default, since
many introductions of partially-revealed types are just done to hide the
names of the method values.


Default parameters
------- ----------

Ian expressed in no uncertain terms his disdain for default parameters.  I
think that M3's default paramters are somewhat better than C++'s, and that,
in any case, default parameters are too useful in the software engineering
process to toss out.

M3 default parameters are better, because M3 allows parameters to a call to be
named.  Thus, unlike C++, I can make a procedure call where all but the last
parameter is defaulted, by naming that parameter in the call.

This isn't entirely a panacea, but it's as close as we're likely to get.  Ian
argues that we could just give the procedure a different name when some of
the parameters are defaulted, but it's hard enough coming up with one
good name for a procedure, let alone two, or four, or thirty-two.  It's
especially hard to come up with names if you have a procedure that's
already using the best name, but which is missing a parameter.  You wind
up either giving the full procedure some awkward name that reminds you
forever after that someone really botched the interface the first time, or
you have to modify all the clients so that they pass a new parameter that
you could have just as well defaulted, or so that they call the defaulted
version of the procedure.  Blecch.

I further contend that default parameters provide much better documentation
for those defaults than you would get through multiple procedure names.


Objects as monitors
------- -- --------

Ian suggests that objects can be treated as monitors, and that this paradigm
by itself makes objects wonderful for concurrent programming.

I'd like to support this, but it's really only the beginning of what you need
to do.

In Trestle, Greg and I carefully document the locking level of fields and
methods of our objects.  Most of our objects *do* descend from MUTEX, and
that MUTEX frequently does protect the fields and methods of that object.

That in itself, however, is inadequate for concurrent programming.  To avoid
deadlock, it is essential that you have some mechanism for capturing the
ordering on your MUTEXes.  In order to support asynchronous computation,
you must have a mechanism for signalling conditions, so that threads don't
have to busy-wait until there's something for them to do.

Anyhow, I believe that M3 is in much better shape on this point than most
other languages I know.  Greg and I are in the process of working on 
automatic tools to verify that programs that use our locking level pragmas
are race and deadlock free.


capitalizationShouldBeSignificant
---------------------------------

This is basically an unimportant surface feature of a language.  At SRC, we've
found that using capitalization conventions to distinguish types, constants,
and procedures from variables, fields, and methods is helpful.  And it avoids
the awkwardness of having to come up with good names for both a type and a
member of the type.  While you can avoid this by suffix conventions (types
are always plural, say), it's easier to admit that Pie = (Blueberry, Cherry)
is a nice name for the type, and that pie := Pie.Cherry is a sensible way to
name a variable.

But no one will ever be convinced who doesn't already believe.


x.incr()
--------

Ian argues that 0-ary procedures shouldn't need parentheses, just as variables
don't.  Well, maybe, but procedure calls don't have L-values.  That is, it's
an abuse of mathematical thinking to think of variables as 0-ary functions.

Moreover, in the case of a method call, it isn't true that the function is
0-ary; it's 1-ary with an implicit argument of the object itself.

Finally, if naming a procedure or method ought to call it, and special syntax
is required to treat it as a procedure variable, then you're going to need
that special syntax in declaring the values for methods.  Blecch.

I suppose that I could be persuaded that () should be allowed after a variable
or field name to convert it to its value.  That is, if x has a field foo,
x.foo() is a non-designator whose value is that of x.foo.


Procedures versus methods, modules versus objects
---------- ------ -------- ------- ------ -------

Ian complains that in C++, the interface designer must designate which methods
are virtual, and which are not.  M3 doesn't have that problem: all methods
are virtual.

But not all procedures that apply to objects are methods.  Some are 
procedures defined at top-level in an interface.

Greg and I will plead guilty to the charges of having done this extensively
in Trestle.  Our defense is that method syntax is only useful for those things
where you want the programmer to think of the thing as an object, and where
the semantics admit some sensible form of overriding.

Thus, painting into a window in Trestle is a procedure call.  A client
programmer should not think of there being any retained state as a result
of this, except the state on the screen.  And there isn't any utility to
overriding this as a method, except to make it slower.  Because, in fact,
there is state inside a window, and the painting calls are buffered up and
then handed off to a method which gets to deal with them several at a time.

The consequence is that the call requires not just the procedure name, but
the module name as well (adding some of that redundancy that Ian generally
approves of), the system runs faster, and no restrictions have been imposed
on the programmer.

A similar case is that of readers and writers, which are carefully described in
Greg's book on M3.  The outer layer exposes only procedures that perform a lot
of boiler-plate, resolving eventually to method calls on a data structure
tuned for better performance.

Call us troglodytes, call us retrograde, call us insensitive to the higher
truth that everything is a method call.  You would be right.  A programming
language should facilitate the design of well-crafted interfaces; it can not
prohibit the construction of bad ones.  Modules and procedures have proven
their value in the past.  Just because everything I can write with them can
be written in pure object notation doesn't make it a good idea to do so.

Unless you want us to delete all the boolean operators except NAND, all the
arithmetic operators except -, ....


Some final issues
---- ----- ------

Ian may find a few more things about M3 that he'll hate.  I hope not, but
who knows.  Here are a few candidates.

1. Wrong number of friends

In M3, given partial revelations, the notion of friends doesn't make too much
sense.  The information hiding is much better than in C++, because you don't
even get to notice when things you shouldn't use change.  But, if the revelation
is made in an interface, there are no restrictions to prevent programmers
from importing those interfaces.  I think this is good: you never know when
you'll want to write a module for, say, data-structure browsing that will
need to access fields that you had meant to share only with a few of your
closest friends.

In large part, this is mitigated by the ability to have a module export
multiple interfaces, allowing you to reveal things inside an implementation
module, if you really don't want anyone else to know about it.


2. Nested scopes, including nested procedures

Hey, we think they're useful every now and then.  Limited scopes for
variables can be very useful; often you can avoid introducing a
variable until you also know how to initialize it.


3. Objects as references only

This seems to be required by safety, if you have either value mode parameters
or structure copy.  While Oberon seems to get pretty far without this
restriction, I would find it strange to pass an object's data fields in
part; the methods of the object might access fields which didn't exist in
the restricted object.  If you never pass objects by value, and object
copy doesn't copy the methods, you might be OK.  But then you have to deal
with the impact on garbage collection, and that can be a little tricky.

At any rate, there are various techniques one might explore that would
give you the benefits of having objects as records instead of as references
to records.  Garret Swart has posted in the past a few restrictions one
could make that would make stack allocation safe.  On a more interesting note,
Henry Baker's recent SIGPLAN paper (Why CONS shouldn't CONS its arguments)
argues that you can use the stack to hold the most recent generation of
stuff that should be on the heap, copying things off by need.  Clearly, a
compiler could often work out when you're going to wind up needing to do that
copy, and could optimize that case a little for you.


Finally, I would list multiple inheritance, but since Ian never mentioned it,
I'll omit it too.

Mark