Harmony classes [Was: Operator overloading revisited]
On Wed, Jul 22, 2009 at 11:30 AM, P T Withington<ptw at pobox.com> wrote:
On 2009-07-22, at 10:52EDT, Mark S. Miller wrote:
Lately Alex and I been reading prog.vub.ac.be/Publications/2009/vub-prog-tr-09-04.pdf. Later today I will post some initial thoughts on how this could also be supported in within the same general framework.
Thanks for this pointer. I am just starting to read and it looks intriguing. Traits that also have state might just make me give up my mixins!
Given the following helper abstractions:
const traitConflict() { throw new Error("trait conflict"); }
const conflictDesc = Object.freeze({ get: traitConflict, set: undefined, enumerable: false, configurable: true });
const isSameDesc(desc1, desc2) { ... }
const addTrait(self, trait, opt_advice) { const advice = opt_advice || {}; Object.getOwnPropertyNames(trait).forEach(const(k) { const newDesc = Object.getOwnPropertyDescriptor(trait, k); if (k in advice) { const k2 = advice[k]; if (k2) { k = k2; /*String(k2); ? */ } else { return; } } const oldDesc = Object.getOwnPropertyDescriptor(self, k); if (oldDesc) { if (isSameDesc(oldDesc, newDesc)) { // already cool } else { Object.defineProperty(self, k, conflictDesc); } } else { Object.defineProperty(self, k, newDesc); } }); }
and using no sugar beyond that defined in the thread starting at esdiscuss/2009-March/009115,
here is the main example from Tom's paper:
class AnimationTrait(self, refreshRate) { const timer = makeTimer(); public start() { timer.everyDo(refreshRate, const() { self.animate(); }); } public stop() { timer.reset(); } }
const ParticleTrait(self, radius, moveRate, dx, dy) { const resultTrait = {}; addTrait(resultTrait, CircleTrait(self, radius)); addTrait(resultTrait, AnimationTrait(self, moveRate), { start: 'startMoving', stop: false }); addTrait(resultTrait, object { public animate() { self.move(dx, dy); } }); return Object.freeze(resultTrait); } const ParticleMorph(radius, moveRate, dx, dy) { var self = {}; addTrait(self, ParticleTrait(self, radius, moveRate, dx, dy)); return Object.freeze(self); }
Once we know whether or not we want to adopt traits, and whether we wish to adopt precisely Tom's traits semantics, we could consider further sugar for supporting exactly this pattern. Until then, and in order to get to such a state of clarity, it is pleasing that traits semantics such as Tom's can be expressed well directly in terms of the currently proposed non-inheriting classes-as-sugar system.
const ParticleMorph(radius, moveRate, dx, dy) { var self = {}; addTrait(self, ParticleTrait(self, radius, moveRate, dx, dy)); return Object.freeze(self); }
Given the additional helper
const complete(MakeTrait) { return const MakeObject(...rest) { var self = {}; addTrait(self, MakeTrait(self, ...rest)); return Object.freeze(self); } }
we can define ParticleMorph as simply
const ParticleMorph = complete(ParticleTrait);
On Wed, Jul 22, 2009 at 11:30 AM, P T Withington<ptw at pobox.com> wrote:
On 2009-07-22, at 10:52EDT, Mark S. Miller wrote:
Lately Alex and I been reading <prog.vub.ac.be/Publications/2009/vub-prog-tr-09-04.pdf
. Later today I will post some initial thoughts on how this could also be supported in within the same general framework.
Thanks for this pointer. I am just starting to read and it looks intriguing. Traits that also have state might just make me give up
my mixins!
I'm very pleased to see that our work is relevant for the Javascript
community. If it's any help, the slides I used to present the paper a
couple of weeks ago at ECOOP09 are available at: prog.vub.ac.be/~tvcutsem/presentations/Traits-ECOOP09.pdf
Some comments on Mark's proposal follow:
Given the following helper abstractions:
const traitConflict() { throw new Error("trait conflict"); }
const conflictDesc = Object.freeze({ get: traitConflict, set: undefined, enumerable: false, configurable: true });
Do I get it right that a trait composition conflict is only raised
upon accessing the conflicting property? In AmbientTalk, the error is
signaled at composition-time (i.e. as if your addTrait method would
raise an exception). What's your rationale for deferring the exception
until the property is accessed?
const isSameDesc(desc1, desc2) { ... }
const addTrait(self, trait, opt_advice) { const advice = opt_advice || {}; Object.getOwnPropertyNames(trait).forEach(const(k) { const newDesc = Object.getOwnPropertyDescriptor(trait, k); if (k in advice) { const k2 = advice[k]; if (k2) { k = k2; /*String(k2); ? */ } else { return; } } const oldDesc = Object.getOwnPropertyDescriptor(self, k); if (oldDesc) { if (isSameDesc(oldDesc, newDesc)) { // already cool } else { Object.defineProperty(self, k, conflictDesc); } } else { Object.defineProperty(self, k, newDesc); } }); }
If I understand correctly, the line
Object.defineProperty(self, k, newDesc);
installs the trait's 'newDesc' property in the object denoted by
'self'. Two issues need be taken into account if the property refers
to a method:
-
the trait method needs a way to refer to the composite, such that it
can invoke required methods. From your example below I infer that
'self' can be made to refer to the composite because you explicitly
parameterize classes with 'self'. This makes sense, especially for
traits which can be seen as 'incomplete classes'. -
if the trait method refers to lexically free identifiers, will these
identifiers be bound correctly if the method is invoked on the
composite object? More concretely, using the example below: if the
method 'start' is invoked on a ParticleMorph, can I rest assured that
the reference to 'timer' in AnimationTrait's 'start' method refers to
AnimationTrait's timer?
I think it works because you represent methods simply as functions
(closures). I assume that newDesc's 'get' propety refers to a function
that represents the trait method, and this function has closed over
its lexical scope and will correctly refer to lexically free
identifiers even when installed in a different object. Am I right? If
this is the case, that's great because it avoids the need for Self-
style delegation, which we required in our paper because methods were
not mere functions (they were implicitly parameterized with 'self',
and delegation was required to late-bind 'self' to the composite).
On Fri, Jul 24, 2009 at 1:08 AM, Tom Van Cutsem<tvcutsem at vub.ac.be> wrote:
On 2009-07-22, at 10:52EDT, Mark S. Miller wrote: Given the following helper abstractions:
const traitConflict() { throw new Error("trait conflict"); }
const conflictDesc = Object.freeze({ get: traitConflict, set: undefined, enumerable: false, configurable: true });
Do I get it right that a trait composition conflict is only raised upon accessing the conflicting property? In AmbientTalk, the error is signaled at composition-time (i.e. as if your addTrait method would raise an exception). What's your rationale for deferring the exception until the property is accessed?
This change of semantics was more exploratory than purposeful. A retroactive rationalization though is that a conflict that isn't used shouldn't need resolution. C++ does something like this for its multiple inheritance. Of course, they detect possible use statically as well, so they don't pay the price of delaying the failure till the use actually occurs.
If desired, it would be simple to alter the scheme I posted so that it signals the conflict at composition time instead. I have no strong opinion on which we should prefer.
const isSameDesc(desc1, desc2) { ... }
const addTrait(self, trait, opt_advice) { const advice = opt_advice || {}; Object.getOwnPropertyNames(trait).forEach(const(k) { const newDesc = Object.getOwnPropertyDescriptor(trait, k); if (k in advice) { const k2 = advice[k]; if (k2) { k = k2; /*String(k2); ? */ } else { return; } } const oldDesc = Object.getOwnPropertyDescriptor(self, k); if (oldDesc) { if (isSameDesc(oldDesc, newDesc)) { // already cool } else { Object.defineProperty(self, k, conflictDesc); } } else { Object.defineProperty(self, k, newDesc); } }); }
If I understand correctly, the line
Object.defineProperty(self, k, newDesc);
installs the trait's 'newDesc' property in the object denoted by 'self'.
Yes.
Two issues need be taken into account if the property refers to a method:
- the trait method needs a way to refer to the composite, such that it can invoke required methods. From your example below I infer that 'self' can be made to refer to the composite because you explicitly parameterize classes with 'self'.
Yes. That's the purpose of the 'self' parameter in that code.
This makes sense, especially for traits which can be seen as 'incomplete classes'.
- if the trait method refers to lexically free identifiers, will these identifiers be bound correctly if the method is invoked on the composite object? More concretely, using the example below: if the method 'start' is invoked on a ParticleMorph, can I rest assured that the reference to 'timer' in AnimationTrait's 'start' method refers to AnimationTrait's timer?
Yes.
I think it works because you represent methods simply as functions (closures). I assume that newDesc's 'get' prope[r]ty refers to a function that represents the trait method, and this function has closed over its lexical scope and will correctly refer to lexically free identifiers even when installed in a different object. Am I right?
Yes, except for one detail. In this case, it is newDesc's 'value' property rather than its get property. Starting with ES5, a property is either a "data property" or an "accessor property". The descriptor of a data property has the form { value: <any>, writable: <boolean>, enumerable: <boolean>,
configurable: <boolean> }
The descriptor of an accessor property has the form { get: <function () -> any>, set: <function (any)>, enumerable: <boolean>, configurable: <boolean> }
Reading a data property is equivalent to reading the value of the 'value' property of a descriptor of the data property's current state. Reading an accessor property is equivalent to calling the value of the 'get' property of a descriptor of the accessor property's current state. The start method in the example is a data property, so its descriptor would be of the first form.
I used an accessor property in my original post only to represent a conflicted property, so the complaint would happen on any attempt to read the property, whether or not the read was followed by a call.
If this is the case, that's great because it avoids the need for Self-style delegation, which we required in our paper because methods were not mere functions (they were implicitly parameterized with 'self', and delegation was required to late-bind 'self' to the composite).
Yes, exactly. In this pattern, as in your paper, lexical variables are not affected by trait composition. For a property 'foo' exported by a trait, within that trait definition it can be referred to either by lexical variable 'foo' or as 'self.foo'. In the first case, it refers to the 'foo' locally defined by that trait itself. In the second, it refers to whatever 'foo' is finally bound to on the self object resulting from the composition of traits.
Do I get it right that a trait composition conflict is only raised
upon accessing the conflicting property? In AmbientTalk, the error is
signaled at composition-time (i.e. as if your addTrait method would raise an
exception). What's your rationale for deferring the exception until the
property is accessed?This change of semantics was more exploratory than purposeful. A retroactive rationalization though is that a conflict that isn't used shouldn't need resolution. C++ does something like this for its multiple inheritance. Of course, they detect possible use statically as well, so they don't pay the price of delaying the failure till the use actually occurs.
If desired, it would be simple to alter the scheme I posted so that it signals the conflict at composition time instead. I have no strong opinion on which we should prefer.
Me neither, as long as it's obvious to the programmer what composition
caused the conflict.
const isSameDesc(desc1, desc2) { ... }
const addTrait(self, trait, opt_advice) { const advice = opt_advice || {}; Object.getOwnPropertyNames(trait).forEach(const(k) { const newDesc = Object.getOwnPropertyDescriptor(trait, k); if (k in advice) { const k2 = advice[k]; if (k2) { k = k2; /*String(k2); ? */ } else { return; } } const oldDesc = Object.getOwnPropertyDescriptor(self, k); if (oldDesc) { if (isSameDesc(oldDesc, newDesc)) { // already cool } else { Object.defineProperty(self, k, conflictDesc); } } else { Object.defineProperty(self, k, newDesc); } }); }
It's really nice that you can specify trait composition in Javascript
using metaprogramming this easily. I checked your implementation
against our implementation of traits in AmbientTalk. There is one
issue that we had to work around, which relates to "default
properties" that are present in every object: we had to exclude such
properties 'by default' since they would otherwise always cause
conflicts. My guess is that if such properties exist in JS, you would
probably set their 'enumerable' field to false to filter them out.
I think it works because you represent methods simply as functions (closures). I assume that newDesc's 'get' prope[r]ty refers to a
function that represents the trait method, and this function has closed over its
lexical scope and will correctly refer to lexically free identifiers even
when installed in a different object. Am I right?Yes, except for one detail. In this case, it is newDesc's 'value' property rather than its get property. Starting with ES5, a property is either a "data property" or an "accessor property". The descriptor of a data property has the form { value: <any>, writable: <boolean>, enumerable: <boolean>, configurable: <boolean> } The descriptor of an accessor property has the form { get: <function () -> any>, set: <function (any)>, enumerable: <boolean>, configurable: <boolean> }
Thanks for the clarification. Could you point me to a page that
explains the rationale behind distinguishing data properties from
accessor properties? At first sight, it appears you don't need both
since accessor properties can easily subsume data properties.
Kind , Tom
On Jul 27, 2009, at 2:56 AM, Tom Van Cutsem wrote:
Could you point me to a page that explains the rationale behind
distinguishing data properties from accessor properties? At first
sight, it appears you don't need both since accessor properties can
easily subsume data properties.
ES5 is a draft standard based on de-facto standards in JavaScript
implementations. Accessor properties, commonly called "getters and
setters", originated almost ten years ago in Mozilla's SpiderMonkey JS
engine. They were reverse-engineered into other engines more recently.
JS, originally in Netscape 2 in 1995, and in ECMA-262 editions 1
through 3 (the last edition before ES5, finalized in 1999), had only
data properties, with class-wise [[Get]] and [[Put]] meta-methods in
the spec but not exposed to programmers.
My inspirations for JS back in 1995 included Scheme and Self, neither
of which subsumes anything like data properties under accessor
properties. What's more, I didn't have time in the early days to
support user-defined accessors, although native ones existed under the
hood in custom objects defined by the implementation (i.e., "the
DOM"). Thus the ES1-3 standards do not describe properties as pairs of
accessor functions, or talk about exposing such functions to
programmers and allowing custom getters and setters to be defined.
Since accessors came later, they are observable. You can tell they're
there using the ES5 meta-programming APIs. They differ from data
properties in other ways; e.g., you can't shadow an accessor property
in a prototype object by assignment. Thus ES5 can't easily or
profitably recast all properties as accessor properties. There would
still be "data properties" even if the spec formalized on top of
accessors.
On Mon, Jul 27, 2009 at 2:56 AM, Tom Van Cutsem<tvcutsem at vub.ac.be> wrote:
const isSameDesc(desc1, desc2) { ... }
const addTrait(self, trait, opt_advice) { [...] if (oldDesc) { if (isSameDesc(oldDesc, newDesc)) { // already cool } else { Object.defineProperty(self, k, conflictDesc); } } else { Object.defineProperty(self, k, newDesc); } }); }
It's really nice that you can specify trait composition in Javascript using metaprogramming this easily. I checked your implementation against our implementation of traits in AmbientTalk. There is one issue that we had to work around, which relates to "default properties" that are present in every object: we had to exclude such properties 'by default' since they would otherwise always cause conflicts.
That's why I put the isSameDesc() call into the conditional above. If the same name is bound to the same descriptions into two different traits being composed -- as would happen for example in a diamond composition pattern -- then there's no conflict.
My guess is that if such properties exist in JS, you would probably set their 'enumerable' field to false to filter them out.
The equivalent of default properties in JS are properties defined on Object.prototype. Until ES5, all object necessarily inherited from Object.prototype, and typically will still. The properties defined by the spec on Object.prototype are indeed defined as non-enumerable. And in ES5, new properties that anyone defines on Object.prototype can (and therefore should) be defined as non-enumerable as well.
But none of that is why we avoid a conflict for such methods. Since addTrait() enumerates properties with Object.getOwnPropertyNames(), this enumerates all own properties whether enumerable or not. An "own" property is a property defined directly on the object, as opposed to an "inherited" property. So we avoid the defaults from Object.prototype but we do compose non-enumerable properties. We dodge the false-conflict problem with the isSameDesc() check explained above.
On 2009-07-22, at 14:14EDT, Brendan Eich wrote:
On 2009-07-22, at 10:39EDT, Alex Russell wrote:
So, depending on your point of view, Harmony classes allow the user to
create objects that have both the same integrity, and the same
limitations, as native objects.
I suppose that's a start. But if they don't even support prototypical
inheritance, I'll never be able to use them. Give them prototypical
inheritance, and I will probably adopt them for their integrity, using
my existing pre-processor to emulate the classes and mixins I really
want.
My only quibble is whether it is confusing to name something so
limited 'class'?
On 2009-07-22, at 10:52EDT, Mark S. Miller wrote:
Thanks for this pointer. I am just starting to read and it looks
intriguing. Traits that also have state might just make me give up my
mixins!