[Grace-core] "Classical" Inheritance for Grace

James Noble kjx at ecs.vuw.ac.nz
Tue Apr 2 07:25:14 PDT 2013


Hi all

so we've been around and around this, there have been lots of
more-or-less baroque proposals - and yes, this is another one -
hopefully simpler than the one before.  This is because I realized -
and then on further reflection - really realized that we had actually
agreed on basically all the important features of "classical
inheritance", and I think we have for a while.  An email from 9 March
basically specced this out - my notes from a conversation in Denver.

This email is my attempt to write this out, being a proposal (or
non-proposal) plus rationale, a couple of questions about additional
semantics, and then a list of issues I'm (randomly) deciding are
orthogonal to the basic design.  What I hope is that with the
brushwood cleared away we will have something we all think is
defensible, and then we can argue about the other bits - or perhaps
even postpone those discussions into the indefinite future...

I had hoped to elaborate all of this in this email, but time & various
other things got in the way. The rest of this email is a description
of the core "classical" semantics, plus quick notes on the rest.

-------------------------------------------------------------------------

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.

-------------------------------------------------------------------------

* 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...)

-------------------------------------------------------------------------

* Stability / Static

We'll need some kind of defintion of what is a static or stable
(Scala's term) reference to a class. 

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 

(Note that this gets us family polymorphism unless we e.g. restrict
all  stable defs to being "final") 

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. 

-------------------------------------------------------------------------

* 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.

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...

-------------------------------------------------------------------------

* 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.

-------------------------------------------------------------------------

* 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.

-------------------------------------------------------------------------

* 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...

-------------------------------------------------------------------------

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.

-------------------------------------------------------------------------



More information about the Grace-core mailing list