[Grace-core] "Classical" Inheritance for Grace

Kim Bruce kim at cs.pomona.edu
Tue Apr 2 13:34:09 PDT 2013


Comments embedded below.

Kim



On Apr 2, 2013, at 7:25 AM, James Noble <kjx at ecs.vuw.ac.nz> wrote:

> ...
> 
> So, in Denver we agreed on what we meant by "classical inheritance"
> before we agreed on (or rather got dragged into) a big debate on
> mechanisms:
> 
> From earlier email 
> Subject: [Grace-core] [less important] Classical Inheritance vs Delegation
> Date:    9 March 2013 7:54:34 PM NZDT
> 
> Some other notes from today's discussion - Kim on "classical inheritance"
> 
>> A class definition is a template for instances
>> - instance variables 
>> - method suite
>> - some kind of parameterised initialisation code
>> 
>> Single Inheritance:
>> A subclass definition includes all of the above class definition
>> - along with a single superclass
>> - its method suite may override the superclass's methods
>> - initialisation code in the superclass runs before the code for the subclass
>>    with "self" bound to the whole new object
>> - initialisation code can make self-calls, these should have the same semantics
>>    as normal method calls
>> 
>> Classes do not _have_ to be objects --- 
>> they can be separate, static, non-first class.
>> 
>> BUT: classes as objects in so far as they are factories - that "new"
>> is a standard method call - is important and good.
> 
> 
> 
> We can keep the text of the current latest spec sections 7.1-7.3 and
> 7.5, putting 7.4 on hold. (Note that the spec was never updated for
> "reverse become" semantics).  We keep the existing syntax:
> 
> class aCat.ofColour ( aColour ) named (aName) {
>  def colour : Colour = aColour
>  def name : String = aName
>  var miceEaten := 0
> } 
> 
> Inheritance again uses the current syntax (annotations are updated
> from the spec)
> 
> class aPedigreeCat.ofColour (aColour) named (aName) { 
> inherits Cat.ofColour (aColour) named (aName)
> 
> var prizes := 0
> method miceEaten is override {0};
> method miceEaten:= (n:Number) is override {return}
> } 
> 
> What are the semantics?  Hopefully what we (what most people) expect,
> what we said above: we make a whole object, including all the methods
> & unitialised fields, and then we run the code in all the class bodies
> top-down, starting with the uppermost superclass (that had better not
> inherit from anything). The result should be an initialized object.
> 
> How do we create objects? - by writing an expression which names a
> class followed by a request for its method.  The current spec includes
> the timeless phrase "The right hand side of an inherits clause is
> restricted to be a class name, followed by a correct request for that
> class's method" - let's keep that for now - though I'll expand on it
> below - and imagine similar wording for object creation: a class name
> (or perhaps, an expression that definitively statically evaluates to a
> "class") followed by a call to that class's method.
> 
> My point is: I think this is pretty much enough. We don't need to say
> anything more - although we should, I think: this leaves too much
> unspecified. But, if we wanted to, we could leave that unspecified and
> be done - I do think this is the "minimal class based design" we've
> been after.  We've pretty much always had it all along.
> 
> The rest of this email discusses various orthogonal issues in
> inheritance, or one (or perhaps some) options for fleshing out this
> design.
> 
> What this discussion intentionally omits is the "philosophical" or 
> "ontological" status of classes, because basically I think that is
> another orthogonal axis --- if we really feel the need to elaborate
> on things, there are several sensible designs that preserve this
> "classical" core.

This seems fine.
> 
> -------------------------------------------------------------------------
> 
> * Multiple Constructor methods:
> 
> In the spec, section 7.4 talks about object/class encoding, motivating
> the encoding - and justifying "handbuilt" classes - for classes that
> need multiple constructor methods.  I figured if we wanted to go to
> classes, we'd need to add in multple constructors, constructors
> calling other constructors, Dart style factory constructors that don't
> construct anything (e.g returning cached objects etc)...
> 
> But we don't need to do that.  Classes are really only important for
> inheritance: for clients just creating and using objects we can just
> make methods that call the constructor method of the class. With only
> a little pre-planning the pseudo-constructor method suite can be
> inherited, by overriding a hook method that actually invokes the
> "actual" class to invoke a subclass.  You can't inherit from those
> other constructor methods, but you don't need to: you can always just
> inherit from the primary class, and it's constructor method.  I could
> even see myself arguing this is a superior design, or at least a
> simpler one.
> 
> But I (eventually) realized, actually after getting back from Denver,
> that we don't even need to do this. This is because Grace is
> structurally typed. In Nominal languages, classes are really
> important: for the integrity of the system each "class" must be
> created in only one place - only one special construct can "brand"
> objects with any particular magic name (well modulo inheritance
> anyway).  But we're structural: this doesn't matter. Even in the
> class-based design, we can have multiple "class" declarations for the
> same logical class. All this works fine; and each class can be
> inherited from, if necessary.
> 
> And then of course there is "Homer's device" (don't you think every
> good language needs its own special device?) inheriting from a
> constructor without adding any fields or methods - classes like
> 
> class origin.new {
>  inherits Point.new(0,0)
> } 
> 
> you can inherit from this just fine - so if you want to inherit from
> different constructors you can do this too.  In other words: we really
> don't need any support for multiple "constructors", and we can
> probably do without inheriting from "handbuilt" classes for most
> practical purposes*.
> 
> (* Michael and I have a paper being shepherded for euroPLoP which I
>  hope will help explore this space exhaustively...)

This seems to work fine, and one can always build up an object factory if you like that style instead:
def pointFactory = object {
    at(x,y) -> PointType {Point.new(x,y)}
    atOrigin -> PointType {Point.new(0,0)}
}

Depending on other decisions, inheritance might be easy or hard from this.
> 
> -------------------------------------------------------------------------
> 
> * Stability / Static
> 
> We'll need some kind of defintion of what is a static or stable
> (Scala's term) reference to a class. 

Is this supposed to replace definitely statically?
> 
> Adaptifing from Cay's Scala book, and not claiming to be complete or
> correct  - a stable refernence path can contain:
> 
> * implicit or explicit self requests
> * super requests
> * requests that resolve to defs or imports
> * requests that resolve to classes 

Examples would help here!
> 
> (Note that this gets us family polymorphism unless we e.g. restrict
> all  stable defs to being "final") 

I'd worry about this.  Can you illustrate with examples?
> 
> This is quite easy to implement and check, even dynamically, but this
> isn't modular as it talks about implementations.  
> 
> -------------------------------------------------------------------------
> 
> * Modularity -  
> 
> Inheritnce isn't modular. For that we need classtypes. So we make
> (optional) classtypes. I think this is discussed in one of the
> messages I posted in Denver, "Traits (13/3/13)". 
> 
> I think it should be possible to allow classtypes to be specified, or
> to dynamically check when they are not, with a static checker that can
> ensure all classtypes are resolved OK. 

With the equivalent of "definitely static" we should be able to infer the types of all classes and hence never write them down.  However, we could have an option of being able to write them if that seemed useful.
> 
> -------------------------------------------------------------------------
> 
> * Packaging
> 
> Currently we write  class C.m { ... }
> 
> so the encoding (if we have an encoding) is 
> 
> def C = object {
> method m {
>     return object { ... } 
> }
> }
> 
> if we don't actually have such an encoding, 
> we still end up needing to talk about stable paths leading to a class object - 
> the effect is the same.
> 
> Alternatively, we could follow O'CAML and have classes be methods (functions)
> vis:  class m { ... }    encoded as the simpler
> 
> method m {
>     return object { ... } 
> }
> 
> This seems less OO, but also has a few advantages, e.g. 
> if were were using multiple syntatic class declarations &
> Homer's Device to declare multi-constructor classes, all the 
> methods could be in the same object, rather than each in their own
> object. 
> 
> def C = object {
> class m(x,y) {  ... {           // primary constrcutor 
> class origin { inherit m(0,0) } //forward to primary constructor
> class unit { inherit m(1,1) }   //forward to primary constructor
> }
> 
> this also works nicely in the object algebra style -- e..g perhaps we
> want a single collectioms module so you write coll.list(...),
> coll.sequence rather than coll.list.new(...).  Certainly the scala
> style  list(3,4,5,)  works quite nicely as a pseudo-"literal",
> looking better to me than say  List.new(2,3,4).  Hmm. Or not.
The problem with Scala is that to the student it is hard to know when you write List(3,4,5) or new List(3,4,5).  It's definitely nicer to have a single uniform syntax.

> 
> We could even allow either version in the class declarations "macro".
> 
> I think we need to think - and experiment - more with modules and
> libraries and how we want to structure them before making this
> decision --- but again, this is orthogonal to the rest of it all.
> 
> -------------------------------------------------------------------------
> 
> * Names 
> 
> I just used "class" in the syntax and semantics because it's what
> we're used to. We could use other names,  "trait" vs "class" vs
> "maker". In the same way we distinguish between e.g. "variable
> declarations" and "slots"  (actual variables) we should distinguish
> between "class declarations" and "class objects" -- or "makers", or
> "maker methods", or whatever.  
> 
> If we can agree on the basic semantics, then we can fight about
> names...

I think it is probably worth keeping the distinction between classes and factory objects, even with the similarity of syntax when invoking them, but I might be able to be persuaded differently.
> 
> -------------------------------------------------------------------------
> 
> * Multiple inheritance 
> 
> Can I please just assert this is orthogonal? We can do traits or
> multiple "inherits" clauses. We could even have Gilad's implicit mixin
> semantics. We'd have to add some kind of "directed super send" with
> some syntax for it, but there are various choices super::foo, or
> whatever.  With inherits clauses, we have the option of adding an "as"
> to make the resends work better.

If we go down this path (and I'd rather not if possible), we should be very careful.  Everyone who has tried multiple inheritance has run into terrible problems.  Show me the examples where we need it and can't find a work around.  It's one of those great obvious ideas whose flaws show up when you actually try to design them.
> 
> -------------------------------------------------------------------------
> 
> * Reflection
> 
> We need stories/semantics about cloning, and delegation, and mirrors -
> and ideally about how objects are made up from "parts" or "slots" or
> "Quarks" as in Tim's long email.  I guess, again, I think the
> reflexive story is orthogonal to the "classical" design here: while we
> want a reflexive story it shouldn't be the only one, nor the first one
> that we teach to people - in fact, most people need to be able to
> ignore it, or That Way Lies Smalltalk.
> 
> I think the main issues there are maintaing the integrity of the base
> object system, what Mark "Object/Capabiity" Miller calls "lack of
> ambient authority".   We should be clear what is "base" and what is
> "meta" - so, e.g., defs can't be rebound by "base" code, full stop.
> 
> In practice, this means that clone/proxy/delegate methods at least
> need to be invoked on the target to be cloned/delegated/etc - 
> so you can't create "vampires" or "clones" without the target object's
> permission: objects don't *have* to support any of those methods
> publically - or alternativly involved via a separable module, so you
> can configure a secure system without say mirrors, or delegates, if
> you need to. The existing mirrors design does this - if you make
> e.g. Michael's inheritable delegate proxy operation on a mirror,
> rather than a base object, that would also preserve security.
> 
> -------------------------------------------------------------------------
> 
> * Initialisation:
> 
> Currently we don't require all vars to be initialised in a
> constructor. Defs have to be because they can't be initialised outside
> --- although we don't have a Java style definite initialisation rule.
> We could add one. We could require all defs (or/and all vars) were
> initialised at the end of the constructor execution, and raise either
> a st or dynamic error if that's not the case.  This could be done per
> class, allowing compilers to omit dynamic checks e.g. if all vars &
> defs are initialised at the end of the constructor.

Keep in mind that we don't have default values available (e.g., no null), so this could be tricky.  Perhaps this is something that we could encourage but not require.
> 
> -------------------------------------------------------------------------
> 
> * Cyclic definitions / placeholders:
> 
> I'd like to tackle cyclic definitions. Newspeak has per object
> parallel slot initialization - its slot list is surrounded by "||"
> characters (a visual pun :-).   
> 
> if you write a recursive defn like 
> 
> || foo = bar ,  bar.  bar = foo | "x" || 
> 
> then inside the "||" Newspeak implicitly builds a future for each RHS
> of the initialisation, and binds them to the names.  This seems
> powerful, a bit magical, but I guess gets the job done.
> 
> Marco's placeholders proposal is less powerful: basically you bind
> completely empty objects - nothing by raw identity - to each name in a
> first pass, and then initialize all the objects in a second pass. The
> catch here is that this is quite weak in practice --- you cannot do
> *anything* to the objects until they are assigned, they don't have any
> methods and you'd just get an "placeholder exception" aka "send
> message to uninitialised object exception". Macro's type system
> statically ensures this doesn't have to happen, by conservative
> restrictions on allowable constructors.   The code above, or parser
> combinators, won't work with placeholders becasue as soon as you send
> a combinator to a "forward reference" the whole thing dies. This is wy
> I write  rule { ... }  around my combinators -- rule takes a block and
> returns a future, I called it "rule" because it looks better with
> grammar rules than writing "future" :-)
> 
> In Grace, I don't think we *have* to be that strict. We already have
> dynamic semantics for individual uninitialised variables - rather than
> whole objects. I think we can build on that, and on the implicit "two
> step" object construction implicit in the classical semantics
> (explicit in Tim's proposal & factory methods & Andrew's separation of
> construction & initialisation) so that we:
> - allow some kind of grouping of slots to be initialised together
> - initialise the structure, the methods on of every object
> - run the initialisation code, calling methods on objects as necessary
>      if we don't it any uninitialised _variables_ (rather than objects)
>      we complete.
> 
> the point here is that things like parser combinators, the syntatic
> sugar methods only rely on their recievers identity, and arguments,
> not variables --- so e.g. very parser defines infix sequence and
> alternative operators:
> 
> method ~(other) {SequentialParser.new(self,other)}
> method |(other) {AlternativeParser.new(self,other)}
> 
> but these only rely on the reciever's identity, their argument's
> identity, and I guess the class of the object being created.
> 
> Syntatically, we could borrow from BCPL and require something like
> 
> def foo = bar ,  bar
> and def bar = foo | "x"
> 
> with an "and" keyword --- although perhpas we should use "also" or
> "with" rather than "and". 
> 
> def foo = bar ,  bar
> also def bar = foo | "x"
> 
> doesn't seem too bad.  And these semantics - still going top down -
> are rather more deterministic than Newspeak --- although in Newspeak,
> every class is created lazily...

I don't really see the need for a special syntax here.  If the defs make sense the object identity will work.  I.e., we can create two nodes that point to each other by something like
def first = Node.new("a",second);
def second = Node.new("b",first);

That should work without difficulty as long as the constructors don't send messages to  the second parameter.  If they do, then it is broken anyway.  In my opinion all defs and vars should be treated as mutually recursive.
> 
> -------------------------------------------------------------------------
> 
> Other points - while in Denver I sent a bunch of other emails about
> related things. Most of these are yet more mostly-orthogonal
> definitions, and I think those emails still apply. I haven't copied
> them in here, but titles & dates are:
> 
> - Traits (13/3/13) - well at least some of it is relevant,
>        like the discussion on Classtypes
> - Lexically Scoped Variables (10/3/13)
> - Values / Traits / Purity / Const / Static... (9/3/13)
> - Abstract methods and classes & Annotations (9/3/13)
> 
> -------------------------------------------------------------------------
> 
> * Ontology / Philosophy
> 
> The stuff I'm ignoring. That's why I put it last. I think we can do
> this in any number of ways:
> 
> * classes are really in a different universe to objects
>     - class & instance "request" are superficially similar
>       (classes can be contained in instances),
>       or we could go to Java-style "new" to call into classes.
> 
> * classes are special kinds of objects - 
>     - they have the constructor a their method
>     - could have other methods too, e.g. for trait composition.
> 
> * classes are almost encoded objects
>     - you can think of classes via the object ... method ... object encoding
>     - but you can only actually inherit from classes
>     - make classe inherit from "trait Class" and you can have e.g.
>       trait composition methods there too; they'd have to be *stable*
> 
> * classes are actually encoded objects -- handbuilt classes.
>     - you can inherit from classes, or from any method that
>          tail-returns an object constructor
>     - ditto can permit other class-side inherited methods
>     - can easily write them in the outer object
>     - can consider the "handbuilt" syntax as a "synonym"
>          for an underlying actual class definition.
> 
> we can choose. Or we don't have to.   I still have a sentimental
> attachment to encoded object nesting (the last option) but we don't
> have to go that way.   Because, the more I think about it, the more I
> realise that for almost all Grace programs, it makes no difference.

My preference would be for the third option, but I believe you are right:  I don't think we need to decide now.

I have to take my wife to a doctor's appointment at 2:10.  I should be back in time for the video conference.  If not, I'll e-mail.

Kim
> 
> -------------------------------------------------------------------------
> 



More information about the Grace-core mailing list