[Grace-core] Classes and inheritance

Kim Bruce kim at cs.pomona.edu
Wed Jul 13 14:34:31 PDT 2011


See some answers below:

On Jul 12, 2011, at 9:13 PM, Michael Homer wrote:

> Hi,
> I'm working on getting these going at the moment, and I've come across
> a few areas that need clarification:
> 
> ...

> (1a) When should the superclass be resolved? It isn't necessarily
> known at compile time, since the binding of <identifier> may change
> dynamically, and it could even change in between instantiations of the
> class. There's no guarantee that it's a constant, and if someone
> writes their own "class" using an object literal, like the spec
> encourages, the "new" method may even return something different each
> time.

The superclass should be resolved at the same time the expression using the superclass is evaluated.  In particular, an object or subclass should not change meaning dynamically just because during its lifetime the value of the identifier holding the superclass changes.

However if C is a variable holding a class then even writing 
    class A {x -> extends C.new(x) ... method m(..){...} ...}
can be problematic as we would need some way of determining whether m overrode an existing method of C.  In the statically typed world, this could be done by explicitly writing the type of variable C (a class type that listed everything accessible to subclasses), though that would be a bit of a pain to write down and would likely limit the value of C to be extremely constrained.  In the dynamic world it would require run-time checks when this is evaluated.

Having defined (and implemented) a statically-typed language where classes are first class, I must admit that aside from theoretical issues, I found little practical use for class variables, parameters, etc.  In my opinion they are more trouble than they are worth.  

As a result, I would be in favor of not having "first class" classes.  Thus I'd allow
	class C {...} 
or equivalently
	const C := class {...}
though I prefer the first ...

but not
	var C := class {...}


> 
> (1b) Related to both of those, there is a typing issue here of some
> sort: the instances of the class should also be instances of the type
> of the instances of the superclass, which I think means something
> complicated for the resolution rules.

If you mean that the objects generated by the subclass should have a type that is a subtype of those objects generated from the subclass, then that should be easy (at least in the absence of MyType or SelfType in the language), assuming that there are reasonable constraints on overriding methods (no new hiding, subtyping parameters, etc.)

> 
> (2) How can you write mutually-recursive classes? Since "class X { }"
> is syntactic sugar for "const X := object { ... }", something like
> this has scope problems:
>  class A {
>    method asB { B.new }
>  }
>  class B {
>    method asA { A.new }
>  }
> Error: reference to undefined identifier "B" at line 2.

To make this work you generally process (the names of) all declarations added in the current scope before evaluating any of them.  

ML (awkwardly) requires you to join the declarations with an "and".  E.g.:

fun f x = ... g ...
and g y = ... f ...

I do NOT recommend that.  Haskell, for example, adopts the rule I suggested above.

Now if you evaluate the right hand sides, you are OK as long as the mutual recursive stuff is in closures.  That is, when you evaluate A above, you don't evaluate B.new as it is in a block.  However, you do record where to find B so that when necessary you can evaluate B.new.

If instead you wrote

const x = y + 1
const y = x -1

then you clearly have a problem as y does not have a value when x is being evaluated.  On the other hand
const x = {z -> x + y}
const y = {w -> if (...) {x(w)} else {...}}
is again fine as the current value of y is not needed in evaluating x.

(Of course one can still write non-terminating computations, but that is a different issue.)

> 
> (3) What is the behaviour of "super"? It can be used as a receiver, so
> any method can be requested on it, but where does that search start?
> Given the concatenative semantics it seems that the only possible
> place is immediately before the current method, which may be
> unintuitive. Concretely, what does this output:
>  class A {
>    method speak { print "Hello" }
>  }
>  class B extends A.new {
>    <override> method speak { print "Goodbye" }
>    method test { super.speak }
>  }
>  B.new.test
> ? B.new ought to be equivalent to:
>  object {
>    method speak { print "Hello" }
>    <override> method speak { print "Goodbye" }
>    method test { super.speak }
>  }
> , in which it's clear the only possible output is "Goodbye", but that
> may not be what people expect from super. The latter could be an error
> "no superclass", but then super has to be eliminated entirely in the
> rewriting.

As I've stated before, I don't like the concatenation syntax, as it suggests that everything is done via search, which is not done in any reasonably efficient OO language and I believe it is more complex than necessary.  The simpler semantics is that to form a subclass, start with the method suite (semantics, not syntax) for the super class, fill those in the corresponding slots for the new subclass, add the new methods and replace the old methods that are overridden.  (Instance variables are even simpler)  Any references to super should access the methods from the superclass, though with self in the body interpreted as the new object of the subclass.

As a result, any reference to super.speak in the subclass B (as in method test) should always execute method speak from A, i.e., print "Hello".

It is important that you take the values of these methods from the superclass and not just their syntax as textual substitution would really mess you up.

Kim


More information about the Grace-core mailing list