[Grace-core] Considering Traits as Objects in Grace

Andrew P Black andrew.p.black at gmail.com
Wed Mar 6 11:20:15 PST 2013


At PDX, in the United Club ... let's see if I can clarify this before I leave for Denver.   It's clear that I shouldn't have released this draft yet, because it's
obviously confusing, but at least it gives me a list of things to explain better in the next revision.

On 5 Mar 2013, at 02:19 , Michael Homer wrote:

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

That's a good summary.

> Methods in a stateless trait can't refer to anything except "self" and
> their own parameters.

No, that's not what I meant.   If we restrict traits to being stateless (and this may or may not be a good idea), 
then trait objects cannot access mutable state.   They can most certainly access other immutable objects, and I'm assuming that
most dialects will be immutable, so they will have access to control structures.   Methods defined inside other methods can access
the parameters of the outer method, in addition to their own parameters.

I suspect that this restriction makes sense only if we actually introduce immutability into Grace as a formal restriction
on the full language.   We haven't yet discussed doing this, although it's been hovering in the background for a long time.


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

With the above understanding, I think that stateless traits can be as useful as they are in Smalltalk.

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

That's right.

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

The other "big idea" in the paper is the separation of the initialization of objects from their creation.
I suspect that most objects won't actually need initialization at all, since they can be constructed "fully formed".
But this remains to be seen; we could get an idea by studying a Java program corpus. 

> It is, though, where you can do registration.
> The initialize method must be called explicitly in the inheritor's
> initialize method.

The example was supposed to show that the initialize method is requested by the factory method, so that the
client does not have to explicitly request it.  Indeed, this is the whole point: in Kim's window example, he does not
want the student to have to remember to request the method that registers the window.

> 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 didn't explain what super meant.  I think that 
	(0) super is the name of the trait object that I'm using
	(1) super.meth1(args) means super^meth1(args) using ambient talk notation, that is,
	     delegate meth1(args) to the object super.

Yes, this is something special that can't otherwise be expressed, since I'm not proposing the ^ operator be 
part of th language.  But then, super has always been something special that can't otherwise be expressed.

Most objects won't have an initialize method, but Michael has spotted a problem here, the solution to which isn't 
described in the paper.   Suppose:

	def bordedShadowed = trait {
		uses bordered + shadowed
		method drawOn(aCanvas) {  // some combination of bordered.drawOn() and shadowed.drawOn()  }
		...
	}

How do we write the drawOn method?   In Smalltalk, we would do this:

	def bordedShadowed = trait {
		uses bordered@(bDrawOn() -> drawOn()) + shadowed@(sDrawOn -> drawOn())
		method drawOn(aCanvas) {  self.sDrawOn()
							 self.bDrawOn()  
		}
		...
	}

This works, so long as bordered and shadowed don't make self requests on methods that are overridden in the combining object.
With real delegation, we can do better, but that would mean that we would have to expose a delegation primitive to the programmer.

And yes, combing methods have to be written explicitly.  The trait model holds that this is an advantage: it means that 
we have a reasonably understandable way of getting the good effects of multiple inheritance.

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

defs are evaluated at object creation time.  A method called initialize runs exactly when (and if) it is requested;
initialization is a pattern, not a language feature.


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

The paper doesn't yet talk about this at all, and I haven't thought about it much.
James knows a lot about this, and I wanted to talk to him about it.
The issue is whether or not the rhs of a def is restricted.  Right now it's not, 
but that leads to the well-known problem of the rhs of a def requesting a self method
that depends on that, or other, defs having already been evaluated.
We could leave defs unrestricted, and say "caveat programmator", but I'm interested in saying
that the rhs of a def (and th stop-level body of an object constrictor) cannot request 
self methods (in fact, they can't mention self at all).

The issue is: how often will that force a programer to use  a var, initialized in a method, rather than 
a def?

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

I'm not sure what Michael is saying here: maybe that if this restriction forces defs to become vars very
often, then it's bad for the idea of creating an immutable subset, and bad for concurrency.  With that I agree 
wholeheartedly.
> 
> 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.

Once again: I obviously need to clarify that initialize is just a method, and it must be requested explicitly. 

> Class inheritance requires the general-objects-as-traits approach, or
> that heritable classes never have state.

Most classes don't have state, so I don't see this as a big problem.

> Without state I don't think
> constructor parameters can ever be used, so I'll assume it's the other
> one.

I had intended that constructor parameters be accessible from the code inside the constructor,
including the methods of any nested objects.

> A class must make its fields mutable and set them during
> initialize if they depend on anything but the constructor parameters.

Well, if it depends on the lexical context, then a field in a class could be set at
class creation time, and would not need initialization at all.
 
> The placement of the super.initialize request is meaningful and can
> affect the behaviour of both downcalls and upcalls,

That's true — just like any other method request

> leading to objects
> that appear to mutate as they are constructed.

Well, mutable objects can mutate, that's true.   The whole point of an initialize method is to mutate something.
I'm missing an implication, I think.
> 
> 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.

When we defined confidential, we didn't have "the two selves" that delegation implies.  I'm not at all sure
whether confidential should prohibit or allow a client to delegate to a confidential method or not.

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

The strict interpretation would say that self can't appear in the body of an object constructor, and thus not in a block 
inside that object constructor.  So, the problem goes away.  If we allow self, then it's going to mean the trait, not the object
that eventually uses it. 

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

Interesting.  I may be able to get GUnit working with this, if I can figure out how to use it.

Now it's gate time ...  sorry for the sloppy writing.


	Andrew

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mailhost.cecs.pdx.edu/mailman/private/grace-core/attachments/20130306/bb89f9b6/attachment-0001.html>


More information about the Grace-core mailing list