[Grace-core] Considering Traits as Objects in Grace

Michael Homer mwh at ecs.vuw.ac.nz
Tue Mar 5 02:19:46 PST 2013


Hi,
I read the draft of this paper in the repository. I'd definitely like
to be able to do most of the things it describes, but I think it
leaves a lot of the problems at hand unsolved. Being able to take
methods from an object or multiple objects and jam them into another
would be useful (especially when constructing dialects), but I don't
think it can replace inheritance as described. I'm going to try to
describe what I think the paper is describing, and then potential
issues that need at least to be decided on.

In the paper, a trait is an object where no method captures local
state other than "self". Another object can "use" a trait, obtaining
its methods, and can "use" a combination of traits created using trait
operations. Methods obtained this way are delegated to the original
trait object, with self bound to the using object rather than the
trait. Trait operations can simply combine the method sets, delegate
particular methods to a particular trait, or mask a set of methods
from a trait. (I believe this is an accurate description, but I may
have missed a nuance).

Methods in a stateless trait can't refer to anything except "self" and
their own parameters. I think that means they can't access the
surrounding lexical scope, which means they can't use any imported
modules, the dialect, or any control structures. Stateless traits
clearly aren't useful in practice then, so we must be using the
general objects-as-traits model. In that, an object may say it "uses"
any other object, and the trait operations can be applied to all
objects. State in an object is shared between it and all objects that
use it as a trait.

There is this "initialize" method in objects, which can't do any real
initialisation when there is no access to state. Immutable fields must
already be initialised. It is, though, where you can do registration.
The initialize method must be called explicitly in the inheritor's
initialize method. Since you're calling it explicitly, it's really
just the same as calling a registerSelf method at the same point,
although there is some advantage in having a standardised name to
chain on. The semantics of "super" seem to become complicated at this
point too - combining two traits is almost always going to require
writing a manual initialize method for the combination.

I'm not clear on where initialize fits in in relation to def fields or
other code included directly in the object. I think it must run after
them, or perhaps be composed of them (but how defs are set then I
don't know).

In a stateful initialize you can also perform the actual
initialisation of your own var fields, but not defs - if you have a
constant field that needs this, you need to make it a var and agree
with yourself not to change it subsequently. This is either a
"read-settled" or "constructor-settled" field of the parent, which is
effectively final after it is set up, but from the perspective of
egal, if that's still around, or concurrency systems, it's mutable and
thence belongs to a mutable object.

The bottom-most initialize method must be called implicitly by the
runtime system. It will re-trigger any parent initialize methods, so
these methods should be idempotent in themselves, except for uses
purely of "self". In the case of registration, "uses something.new"
should register both the parent and child separately if the semantic
model is consistent, but that could be fudged.

Class inheritance requires the general-objects-as-traits approach, or
that heritable classes never have state. Without state I don't think
constructor parameters can ever be used, so I'll assume it's the other
one. A class must make its fields mutable and set them during
initialize if they depend on anything but the constructor parameters.
The placement of the super.initialize call is meaningful and can
affect the behaviour of both downcalls and upcalls, leading to objects
that appear to mutate as they are constructed.

Traits are susceptible to the "vampire" problem, where an object's
encapsulation can be broken open by another. If you can "use" a
general object then the visibility annotations never provide any
actual protection at all. That is because you can always define an
object that "uses" the trait or other object, which will then have
access to its confidential methods. The confidential annotation is
then just advice that stops you calling the method directly, so it
only enforces a public type for the object rather than encapsulating
the implementation. That may not be a real problem, depending on how
you conceive of an object, but we would need to consider the
implications pretty carefully.

I'm not sure about the block-self problem that brought up our
inheritance issues in the first place: from one perspective it's the
same answer as before, of not capturing self when you don't mean that
lexical object, and from another you should be putting it inside the
initialize method instead. I don't really mind the second answer in
general, but it's no actual improvement. It is a step backwards from
the reverse-become approach for that case.

There is also an open question of how these trait operations are
represented in concrete syntax. + and - aren't actually available but
these operations will probably come up often once they're in the
language. What is the syntax for method sets and maps? Are those
collections first-class?

As an aside here, I currently have an "is parent" annotation
implemented (on def fields only), which gives essentially the
single-delegation behaviour described. That was a hack to make dialect
implementations simpler, but it provides some basis for
experimentation. A library could perhaps provide object combinators,
which could then be parent and get most of the effect of this design.
-Michael


More information about the Grace-core mailing list