[E-Lang] Re: inheritance, delegation, mental models for
beginners (was: down with `define' (was: newbie syntax: picayune
points from a prejudiced programmer))
Mark S. Miller
markm@caplet.com
Fri, 02 Mar 2001 15:55:41 -0800
At 11:01 AM Friday 3/2/01, Marc Stiegler wrote:
>[With inheritance] if the subclass overrides a
>method in the superclass, when the superclass calls that method it goes back
>out to the subclass's method. With delegation, a delegated-to class call to
>the overridden method is directed to the delegated-to class implementation.
As Dean's message reveals, I may be badly out of touch with how the world
uses terms these days. However, my use of these terms agrees with MarcS's,
which I believe agrees with the website and the list. Therefore, when you say
> The MUD programming languages [...] use the word
> "inheritance" to mean the thing that we call "delegation".
followed by
> Since E is dynamically typed there is no really important difference, is
> there? [2]
since the second doesn't correspond to our usage, I remain curious about
what MUD programmers actually do mean by "inheritance".
Regarding academia's use of these terms, perhaps the definitive document is
the Treaty of Orlando
http://lieber.www.media.mit.edu/people/lieber/Lieberary/OOP/Treaty/Treaty.ps
>In fact, I have thought about designs for a few programs for which
>inheritance would make the system easier to understand and maintain, [...]
I've got one for you: the shamefully undocumented CopyVisitor from within
the E language implementation
http://www.erights.org/javadoc/org/erights/e/elang/visitors/CopyVisitor.html .
Given a tree of typed nodes, such as Kernel-E parse trees, we know that
we'll want to write several algorithms for walking such trees and returning
similar trees derived from these trees by some transformation. The current
E language implementation has three such tree transformers: RenameVisitor,
AlphaRenameVisitor, and SubstVisitor. For all of these, for most of the
types of parse nodes, they simply want to copy that parse node while
*filling in the likewise transformed version* of the children of that parse
node.
The copying behavior they have in common is implemented by the CopyVisitor.
The CopyVisitor is carefully written so that it can be reused in an
equivalent fashion whether by Java-style class-based-inheritance, or by
E-style inheritance-by-static-delegation
http://www.erights.org/elang/blocks/inheritance.html .
The familiar class-based description first:
The actual RenameVisitor is implemented in Java as a subclass of
CopyVisitor. It overrides those methods it needs to change, but not the
others. For these others, CopyVisitor's copying behavior is used, which
first *calls itself* to make a copy of the children of the node being
copied, and then makes a copy of the current node using these copied
children. Of course, the "itself" above is actually a RenameVisitor rather
than a pure CopyVisitor, so these children contain transformed copies of the
original children.
The equivalent instance-based delegation description:
RenameVisitor could have been implemented in E as follows:
def newRenameVisitor(renamings) :any {
def self := {
def super := newCopyVisitor(self)
def RenameVisitor {
to visitCatchExpr(...) :any { ... }
//... likewise for all parse node types
// needing transformation...
delegate { super }
}
}
}
CopyVisitor could have been implemented in E as:
def newCopyVisitor(self) :any {
def CopyVisitor {
to visitCallExpr(recip, verb, args) :any {
def transformedRecip := recip welcome(self)
//... transform args ...
newCallExpr(transformedRecip, verb, transformedArgs)
}
//... other parse node types ...
}
}
This can be seen as a combination of visitor and decoration, or we can just
name this pattern "inheritance", since it exactly produces (with
instance-based delegation) the semantics associated with class-based
inheritance.
Given that we keep "delegate", notice how the "class" construct provides us
no additional help with the above code. Why? Because RenameVisitor is (in
Java terminology) concrete and final, while CopyVisitor is essentially
abstract. The class construct only helps in defining the equivalent of
classes that are concrete and non-final. The price of leaving out the
"class" construct is only that it becomes four lines more difficult to write
one object-maker that supports both direct instantiation and inheriting (be
delegation) from the objects it creates.
Btw, many of us, when doing class-based programming with inheritance, mostly
stopped using concrete non-final classes since the Udanax Gold days. When
we found we wanted to subclass a concrete class, we refactored so that we
were subclassing from an abstract class. I believe Dean was always the
biggest advocate of this design rule. The E implementation grossly violates
this, but that's only because of Java used to have a horrendous huge
per-class memory cost. (It may still, but we should fix this anyway, since
memory is now cheaper.)
So my current inclination is to leave out the "class" construct, go with
MarcS's definition of "inheritance", rather than Zooko's and to truthfully
observe that E supports inheritance perfectly well. And then expand on this
in the Walnut, including the qualifier about concrete non-final classes.
On a different and depressing note, in writing the above code, I noticed a
reason not to use the "newThing" convention: I first wrote
def newRecip := recip welcome(self)
and then I caught myself. I think this mistake will always be natural.
Better not to have a convention that conflicts with the above line. But I
really dislike "classThing". I like "ThingClass" only slightly better. Are you
sure you don't like the current "ThingMaker"? Instead of either pretending
it's all just classes, or throwing them into the deep end of lambda
concepts, can't we introduce Makers by saying:
>Makers in E are like classes. You define one as follows. [all Makers have
>a "Maker" suffix on their name.] You use one as follows:
>
> def foo := ThingMaker(...)
Only later need we reveal that the ThingMaker pattern is only a pattern, not
class-like magic.
Cheers,
--MarkM