[Grace-core] Revising factory inheritance

Michael Homer mwh at ecs.vuw.ac.nz
Thu Mar 14 20:40:13 PDT 2013


Hello,
I am increasingly leaning towards the "factory method"/"satan"
rendition of inheritance. While it is pretty horrible for all the
reasons we know, it's at least explicably horrible, which is more than
can be said for most of the other options that get most of the way to
what we want. I also believe with these moderate changes we can make
it satisfy all of our longstanding criteria:
    PICK TWO:
    1. self requests ("downcalls") in constructors work normally
    2. we can inherit from arbitrary third party objects
    3. we have the simple explanation of classes in terms of objects
without complicating the language any further.

I have a proposal to amend this later in the message, but for the
moment I'm defining the "factory method" system the same way that it
has been before, as:
1) You can only inherit from methods that tail-return "fresh" objects.
Returning a fresh object means either that the return value is an
object constructor expression or a request of another method that
returns a fresh object. A method returning a fresh object is a factory
method.
2) When you inherit, the object identity is created first, and
implicitly passed up the chain to the request in the "inherits"
clause. Factory methods are compiled twice: once with, and once
without, the implicit parameter. A factory method that tail-returns
another factory method passes on the additional parameter to the inner
call as well (this is the really horrible part - but see below for a
subsidiary proposal).
3) Each layer of inheritance adds its methods to that identity as it
goes from bottom to top.
4) Field initialisation and any inline code executes top to bottom,
with the object that isn't inherited by anything last.

This definition gives, in essence, classical inheritance semantics
with the classes-as-objects encoding we've assumed to date. The magic
is confined to that implicit self pointer, which will generally
conform to what people expect anyway and can be easily explained in
exactly those terms. We don't have to include traits, proper classes

How can that resolve our other problems? In particular, Andrew wants
to inherit from arbitrary pre-existing objects, patterns want to
inherit from booleans, and dialects at least want to include other
modules in themselves.

We can achieve that by defining a method which I'll call delegate(x :
Object). delegate returns a fresh object with all the same method
signatures that x has. It does not contain the methods themselves;
rather, each method in this fresh object calls and returns the
corresponding method in x with all of its parameters. x, like any
method parameter, is fully realised before being passed.

In this way we can have code like this:
def someTrait = object {
    method foo(y) {
        y + 1
    }
}
...
object {
    inherits delegate(someTrait)
    print(foo 0)
}
which, as far as we can tell as the author of the code, does
straightforward delegation exactly as my "is parent" prototype does.
Optionally - and this is largely orthogonal to the proposal - delegate
can be made to generate delegating methods that perform pseudo-self
requests, and so having and giving access to confidential methods, or
it can not. If it does not, and only wraps public methods, we avoid
the worst of the "vampire" problem. One reason to permit access to
confidential methods is to allow a dialect to provide the entirety of
the other dialect it is inheriting. The overall system stands
regardless.

We could also have another method, concatenate, giving concatenation
semantics (copying the object into the new one) if those are desired.
Another extension would allow the current "reverse become" semantics
as well, where "inherits become(x)" would again fully resolve x, but
retain the final self pointer as it did so. Other methods and
libraries could construct fresh objects with reference to multiple
existing ones, to implement the trait algebra that Andrew wants.
Crucially, these methods are not part of the semantics of the language
or of inheritance - they're just methods, whose implementation is
encapsulated.

As I said, I don't like the magic threading through factory methods. I
would like it if we could change my definition 1 above to:
1) You can inherit from methods tail-returning "fresh" objects.
Returning a fresh object means that the return value is an object
constructor.
You can make that change without losing anything, because what would have been:
  method someFactory {
    someOtherFactory(1, 2)
  }
can be changed to
  method someFactory {
    object {
      inherits someOtherFactory(1,2)
    }
  }
without loss of generality. Only inherits then ever needs to make the
implicit-parameter request. It also means that what is and isn't a
factory method can be inferred with only local knowledge. While this
isn't totally essential to the overall proposal, I think it improves
upon the basic factory system and simplifies the explanation and the
model significantly.

With this proposal we get a single, fully consistent, model of
inheritance. We achieve other behaviours we might want the same way we
do everywhere else in the language - by method requests. The semantic
model is clear and readily explicable, but for most cases - and for
most students - it won't need to be explained. Through the delegate
method a fully objects-first approach is possible.

With reference to the criteria set out earlier:
    PICK TWO:
    1. self requests ("downcalls") in constructors work normally
    2. we can inherit from arbitrary third party objects
    3. we have the simple explanation of classes in terms of objects
1 works because we have bound the methods to the object identity
before we execute any code. 2 works using the delegate method to give
us a fresh object. 3 works because "inherits" wraps up the limited
special case for us, and what we write is exactly the same as it has
always been. Other problems we've had - registration, self-capture
during initialisation, etc - also don't come up, because we have a
single object identity throughout. So we have picked all three and
lost next to nothing in the process.

I think this works. I don't overly relish implementing it, more
because of previous design decisions than the details of this design
itself, but it is achievable. With the modification I proposed it is
simple and has very restricted special behaviours. I think we should
pick this and move ahead with it.
-Michael


More information about the Grace-core mailing list