[Grace-core] Inheritance and object initialisation

James Noble kjx at ecs.vuw.ac.nz
Sun Jul 22 04:08:14 PDT 2012


On 21/07/2012, at 09:27 AM, Kim Bruce wrote:
> I'm depressed.  

yep, me too. Or at least, I think we perhaps need to rethink chapter 3 of the Onward! paper (when's that due?)

> I believe that there is no way to define inheritance for objects that gives what I consider to be the right behavior for classes.  It seems to me that the problem is that after an object comes into existence, it may mutate in many ways and when you extend it, presumably you are extending the current state of the object, so re-running constructor code would be wrong.

Right. Re-running constructor code isn't going to make sense.  
(* but see "A possible hacky solution" for a way to ensure you
can always re-run a constructor from which you inherit)

After the teleconference we added this to the scratchpad googledoc:

> PICK TWO:
>  1. subClass.new prints "world"
>  2. we have object inheritance
>  3. we have the simple explanation of classes in terms of objects (or vice versa)
> 
Basically we can have two of: self binding "correctly" to subobjects
in object constructors; inheritance from general, pre-existing
objects; or the classes-as-objects encoding.  It seems our design task
is to choose which of these to omit.

Drop any one of these requirements and everything works.  Inheriting
from any object and the class encoding can work fine with delegation -
that's what minigrace does now - but self in object constructors must
always be bound to the immediate object being constructed: not to some
(or rather many) potential future sub-objects.  Drop object
inheritance - so the "inherits" clause must *always* refer to a class
and invoke its primary constructor, and you can bind self wherever
you'd like it, and still (more or less) explain those classes in terms
of the object encoding - it's just that the inheritance story has got
a bit more complicated.  Keep the self binding in classes doing what
you like, and keep object inheritance, well now you can't explain
class inheritance in terms of object inheritance: you have two
different mechanisms.


> So now the issue for me is whether or not there is a reasonable way of defining inheritance for objects that is simple to understand and that provides useful behaviour.


 
So the short answer is: delegation. Which is what we have in
minigrace.  But it doesn't get us the "class like" behaviour of
inherited object constructors.

This is - partly - all old hat: Henry Liebermann's "Using Prototypical
Objects to Implement Shared Behavior"; Lynn Andrea Stein's "Delegation
is Inheritance"; the "Treaty of Orlando" are pretty well known -
although they aren't (yet!) in the repo, that's because I'm hope on
Sunday evening.  But I don't think any of these papers addressed the
questions we're facing: modelling classes as objects, and in particular
the strengths and weaknesses of object constructors.

Why delegation, and why not Taivalsaari-style concatenation?  (That
paper is in the repo).  Delegation has lots of problems, notably with
initialisation, requiring cloning methods, etc, prototype corruption -
how do we escape those?  My answer (so far) is because we've been
lucky, and because we generally code in quite a "class-based" fashion,
so avoid many these issues.  The answer is, also, that we avoid other
problems found in Self or Antero's language Kevo because we have
generative object constructors, rather than singleton object literals
like Self, Scala & Newspeak.

And if you always delegate to a fresh, otherwise unreferenced object -
which is pretty much what we do in Grace, then delegation more-or-less
models class inheritance... except when it comes to initialisation
(i.e. point 1, which is what we're worrying about).  Where do those
fresh, unreferenced objects come from --- from calls to object
constructors typically embedded in classes.  If we want to inherit
from *any* general object, then delegation semantics are basically the
ones that make the most sense --- and we will get all the problems of
delegation that we don't want.

A Digression about Cloning: 

Some of the solutions we have talked about introduce implicit shallow
or deep cloning to get around these problems: basically you clone the
super-object so that it is fresh, and at that point you can delegate
to it --- or somehow copy the slots into the new object - if the
reference is fresh the semantics are basically indistinguishable.  I'm
not much in favour of this, for several reasons.  Shallow cloning
seems the obvious option at first: it's simple, it's semantics are
clear, and in fact we already support general syntax for shallow
clones in Grace :-)

object { 
  inherits target 
} 

But of course a little more thought shows that shallow cloning equally
will not do for all the well known reasons, object's internal
structures, or any external resources, will be shared in a way that
breaks their invariants (think of shallow cloning a list, or even a
vector, sharing the underlying representation; or cloning a file
handle...). You could add ownership (I'd like to anyway) or use
nesting to indicate what should and should not be cloned (clone
"insides"): I think this solution can work for encapsulated internal
state, but this still needs programmer intervention for external
resources.  [Should crib Paley's paper for the references here] So
that seems to require a model with delegation and explicit cloning.

My biggest reservation about adding cloning as an explicit concept
into Grace's object model is that there is a perfectly good cloning
based object-model for OO languages: Self.  Once you start drawing
box-and-arrow object diagrams it's relatively simple to understand and
to teach.  And I even think we should be able to kludge together a
relatively simple model that looks quite a lot like classes ---
basically by renaming the "clone" method "new".  Andrew: isn't this
what Dave Ungar as done in Ly or SLy?  I haven't thought through all
the details, but this is almost certainly one option worth following
up, but it's quite a big jump from where we are now.  This is because
cloning and delegation are enough by themselves, they work fine with
singleton object literals.  We wouldn't need the Emerald style
generative object literals.  I even think the "virtual superobjects"
required for family polymorphism could probably be made to work: but,
again, I haven't worked out the details.   If anyone's keen, we could
try this out in Self and see how far we get.


A Possible Hacky Solution:

The only other option I can think of so far would remove point 2 & 3 -
removing object inheritance, giving us the class behaviour we want,
but also supporting a (mostly) simple description of classes in terms
of generative object literals.   The trick here is perhaps the trick
Kim's been pointing to all along:  saying you can inherit from a
"class-like structure" --- for want of a better term I'll call it a
"classoid".  A classoid is: 
 - the result of a class declaration
 - a method that returns an object constructor *as a tail-call*
 - a definitively static (i.e. **final**) tail-call to a classoid :-)
 - (potentially a self-call to a classoid, if we're willing to re-evaluate
	 "virtual superclasses")

This basically avoids all the cloning by using generative object
constructors, and then _requires_ the "inherits" call to statically
resolve to a *dynamically invoked generative object constructor*.
That can go through a bunch of other stuff as long as an object
constructor is eventually reached in the tail position of some method
--- and any method calls are also tail-calls.  At this point, we can
run the object constructor ***in the context of the "whole" object we
are constructing*** -- the whole object here being both of them
super-object and the sub-object, really both of them *concatenated
together.  This can give us the behaviour Kim wants; is build out of
the generative object constructors that Andrew wants; and more-or-less
aligns with the spec and even the Onward! paper :-) In semantic terms,
well yes as Kim suggested, it must be treating object constructors ---
at least those object constructors it is possible to inherit from ---
as classes, rather than objects, thus the insistence of the object
constructor appearing in tail-position of a chain of tail-calls: the
super object constructor is always being run in a lexical context
where its bindings make sense --- there's no attempt to "re-execute" a
constant singleton object.   Rather, we just ensure any object constructor
from when we wish to inherit *can always be "re"-executed* - we 
forbid inheritance from those that cannot be re-executed.

One thing this means is that objects intending to be inherited from
--- even things like "true" --- have to be defined in methods, rather
than as defs holding constant objects. In fact we could weaken the
definition of a "classoid" with another base-case: a *stateless trait*
--- that is an object that purely defines methods, not state.  (Yes,
nesting greatly confuses the issues here: and yes, I'm ignoring that
confusion for now).  If we keep the EGAL rules for objects, then 

> method a { object { some stateless trait } }

and

> def a = object { some stateless trait } 

pretty much mean the same thing, because if the object is stateless,
every constructor call can returns the same object, or at least the
objects are indistinguishable.  The other advantage of being
stateless, there's no initialisation to worry about.  Of course 
once objects are not stateless, the inheritance rules will
strongly (and apparently arbitrarily) distinguish the two!

Without or without the stateless trait exception, I think this can be
made to work. But it does mean that we're adding a nasty twist into
object constructors, basically making them classes, and turning what
were some implementation-specific restrictions (the definitively
static rules were really there to let implementations use static
object layouts without doing the full tracing JIT thing) into
important semantic rules about which programs make sense,
nasty rules that carve through encapsulation barriers, across
module boundaries, between "compile time" and "link time",
and into the core of the language itself.

James 



PS: experimentally, for some more exotic things, we could always keep
the delegation semantics around that really can "inherit" from
anything - and just use a different keyword - "delegates" perhaps - or
even go for the Self-style

 def p is parent = blah

which would let us support multiple delegation if we wanted to. But
that's another story altogether!  (m is viaParent(p)) or something...

James



More information about the Grace-core mailing list