Proposal: making use of Trait.prototype

12 views
Skip to first unread message

Tom Van Cutsem

unread,
Oct 11, 2010, 2:13:40 PM10/11/10
to traits-js
I've been thinking about the changes that Irakli recently described to his customized version of traits.js. The idea of defining 'create' and 'resolve' methods on Trait.prototype do seem appealing. Here's a proposal for a small change to the library (full credit for this idea goes to Irakli Gozalishvili):

Make trait property descriptor maps created by Trait({…}) inherit from Trait.prototype, where Trait.prototype has the following methods:

Trait.prototype.create = function(proto) { return Trait.create(proto || Object.prototype, this); }
Trait.prototype.resolve = function(resolutions) { return Trait.resolve(resolutions, this); }

Pro:

More terse syntax. One can now write:
var t  = Trait({…});
var t2 = t.resolve({…});
var o  = t.create();

instead of:
var t  = Trait({…});
var t2 = Trait.resolve({…}, t);
var o  = Trait.create(Object.prototype, t);

Also, one can distinguish trait property descriptor maps using:

if (t instanceof Trait) { … }

As Irakli noted, power-users could use the 'create' method as a hook to specialize trait creation, although I'm not sure how useful this would turn out to be.

Con:

Violates stratification in the sense that 'create' and 'resolve' now become part of the namespace of the trait (although not quite, read on). If a trait accidentally defines properties named 'create' or 'resolve', they will override the methods on Trait.prototype. However, since property descriptor maps only map names to _property descriptor objects_ -- which are not normally callable -- calling trait.create(…) on a trait that overrides create will likely throw a type error stating that trait.create (a property descriptor) is not callable, thus warning the user about the collision. The user can then switch to the longer Trait.create(proto, t) syntax which will correctly instantiate 't' even if it defines a 'create' or 'resolve' property.

Furthermore, since trait property descriptor maps currently inherit from Object.prototype, the stratification issue already arises today with methods like 'hasOwnProperty' and 'toString'. Normally, I'm wary of stratification issues like this, but in this particular case what saves us is the combination of the following:
a) only the own properties of a trait are considered by any of the trait operators defined in traits.js, inherited properties are always ignored.
b) traits are only supposed to define data properties, never methods 

I'm not proposing to also define Trait.override and Trait.compose as methods on Trait.prototype, because these work with an arbitrary number of traits. It feels strange to single out 't1' in 't1.compose(t2,t3)'. Also, t1.compose(t2) makes compose feel more like an asymmetric operator, while it is a symmetric (commutative) operator.

I know this is a fairly small community, but I'd like to hear your opinions nonetheless.

Cheers,
Tom

Irakli Gozalishvili

unread,
Oct 12, 2010, 8:37:50 AM10/12/10
to trai...@googlegroups.com
Hi Tom,

Please see my comments inline

On Mon, Oct 11, 2010 at 20:13, Tom Van Cutsem <tomvc.be@gmail.com> wrote:
I've been thinking about the changes that Irakli recently described to his customized version of traits.js. The idea of defining 'create' and 'resolve' methods on Trait.prototype do seem appealing. Here's a proposal for a small change to the library (full credit for this idea goes to Irakli Gozalishvili):
 

 
Glad your considering this :)
 
Make trait property descriptor maps created by Trait({…}) inherit from Trait.prototype, where Trait.prototype has the following methods:

Trait.prototype.create = function(proto) { return Trait.create(proto || Object.prototype, this); }
Trait.prototype.resolve = function(resolutions) { return Trait.resolve(resolutions, this); }

Pro:

More terse syntax. One can now write:
var t  = Trait({…});
var t2 = t.resolve({…});
var o  = t.create();

instead of:
var t  = Trait({…});
var t2 = Trait.resolve({…}, t);
var o  = Trait.create(Object.prototype, t);

Also, one can distinguish trait property descriptor maps using:

if (t instanceof Trait) { … }

As Irakli noted, power-users could use the 'create' method as a hook to specialize trait creation, although I'm not sure how useful this would turn out to be.


I actually invalidated that statement later cause I was wrong ( As you mentioned below custom create will be actually be an object with a property value that is the custom create function :)
 
Con:

Violates stratification in the sense that 'create' and 'resolve' now become part of the namespace of the trait (although not quite, read on). If a trait accidentally defines properties named 'create' or 'resolve', they will override the methods on Trait.prototype. However, since property descriptor maps only map names to _property descriptor objects_ -- which are not normally callable -- calling trait.create(…) on a trait that overrides create will likely throw a type error stating that trait.create (a property descriptor) is not callable, thus warning the user about the collision. The user can then switch to the longer Trait.create(proto, t) syntax which will correctly instantiate 't' even if it defines a 'create' or 'resolve' property.


Actually I experimented with something that is hacky but I think it is fine to use it in a rare cases like this. Since functions in js are objects you can use them as a property descriptor in example it is:

var keys = Object.getOwnPropertyNamse(object)
keys.forEach(function(key) {
   var descriptor = Object.getOwnPropertyDescriptor(object, key)
   if (key in Trait.prototype) {
      var specialDescriptor = function() { return Trait.prototype[key].apply(this, arguments) }
      for (var name in descriptor) specialDescriptor[name] = descriptor[name]
      descriptor = specialDescriptor
   }
 
This way there will no longer be special keys in prototype. I have not landed this yet but I've tested it and works as expected with Object.create / Object.defineProperties.

Furthermore, since trait property descriptor maps currently inherit from Object.prototype, the stratification issue already arises today with methods like 'hasOwnProperty' and 'toString'. Normally, I'm wary of stratification issues like this, but in this particular case what saves us is the combination of the following:
a) only the own properties of a trait are considered by any of the trait operators defined in traits.js, inherited properties are always ignored.

It also the case with my opinionated implementation.
 
b) traits are only supposed to define data properties, never methods 


I don't really got this one can you explain it bit more
 
I'm not proposing to also define Trait.override and Trait.compose as methods on Trait.prototype, because these work with an arbitrary number of traits. It feels strange to single out 't1' in 't1.compose(t2,t3)'. Also, t1.compose(t2) makes compose feel more like an asymmetric operator, while it is a symmetric (commutative) operator.


BTW In my implementation I moved functionality of `Trait.compose` to `Trait` function itself. I'm also thinking of removing `override` entirely since:

1) With a zipped syntax it's pretty easy to resolve traits
2)  This encourages more modular and IMO better design. 
 
I know this is a fairly small community, but I'd like to hear your opinions nonetheless.

Cheers,
Tom

BTW If you would like to look at details of my implementation you can find them here:
http://github.com/Gozala/light-traits/blob/master/lib/traits.js

Regards
--
Irakli Gozalishvili
Web: http://www.jeditoolkit.com/
Address: 29 Rue Saint-Georges, 75009 Paris, France

Tom Van Cutsem

unread,
Oct 12, 2010, 2:16:23 PM10/12/10
to trai...@googlegroups.com
Violates stratification in the sense that 'create' and 'resolve' now become part of the namespace of the trait (although not quite, read on). If a trait accidentally defines properties named 'create' or 'resolve', they will override the methods on Trait.prototype. However, since property descriptor maps only map names to _property descriptor objects_ -- which are not normally callable -- calling trait.create(…) on a trait that overrides create will likely throw a type error stating that trait.create (a property descriptor) is not callable, thus warning the user about the collision. The user can then switch to the longer Trait.create(proto, t) syntax which will correctly instantiate 't' even if it defines a 'create' or 'resolve' property.


Actually I experimented with something that is hacky but I think it is fine to use it in a rare cases like this. Since functions in js are objects you can use them as a property descriptor in example it is:

var keys = Object.getOwnPropertyNamse(object)
keys.forEach(function(key) {
   var descriptor = Object.getOwnPropertyDescriptor(object, key)
   if (key in Trait.prototype) {
      var specialDescriptor = function() { return Trait.prototype[key].apply(this, arguments) }
      for (var name in descriptor) specialDescriptor[name] = descriptor[name]
      descriptor = specialDescriptor
   }
 
This way there will no longer be special keys in prototype. I have not landed this yet but I've tested it and works as expected with Object.create / Object.defineProperties.

I see, so you are merging the method on Trait.prototype with the user's property descriptor attributes. That's clever, but I would prefer not to do this because the library currently does not copy property descriptors anywhere. The user can thus always rely on the fact that traits contain the property descriptors that were provided. I think the chance for confusion is sufficiently small that the above is not necessary.
 
Furthermore, since trait property descriptor maps currently inherit from Object.prototype, the stratification issue already arises today with methods like 'hasOwnProperty' and 'toString'. Normally, I'm wary of stratification issues like this, but in this particular case what saves us is the combination of the following:
a) only the own properties of a trait are considered by any of the trait operators defined in traits.js, inherited properties are always ignored.

It also the case with my opinionated implementation.
 
b) traits are only supposed to define data properties, never methods 


I don't really got this one can you explain it bit more

What I meant was that the following is not a valid trait:

{ foo: function() { return 42; } }

Traits are property descriptor maps, i.e. objects that map property names to property descriptors, so one would need to write:

{ foo: { value: function() { return 42; }, enumerable: true, ... } }

Hence, traits don't normally expect to see a property bound directly to a function. Of course, as you show in your code snippet above, it's definitely possible for a function to be a valid property descriptor if it defines the necessary attributes, but I expect this to be a very rare case, so the chance for confusion remains small.
 
 
I'm not proposing to also define Trait.override and Trait.compose as methods on Trait.prototype, because these work with an arbitrary number of traits. It feels strange to single out 't1' in 't1.compose(t2,t3)'. Also, t1.compose(t2) makes compose feel more like an asymmetric operator, while it is a symmetric (commutative) operator.


BTW In my implementation I moved functionality of `Trait.compose` to `Trait` function itself. I'm also thinking of removing `override` entirely since:

1) With a zipped syntax it's pretty easy to resolve traits
2)  This encourages more modular and IMO better design. 

I'm not yet warmed to the idea of generalizing Trait.compose to loosen its type to take either traits or normal objects and perform the object->trait conversion implicitly. But I agree that if you make that decision, there is no need for both Trait(...) and Trait.compose.

I agree that Trait.override is redundant and can be removed, although Trait.override(t1,t2) more directly states what the programmer's intent is than Trait.compose(t1, t2.resolve({ a_common_property: undefined }))
 
I know this is a fairly small community, but I'd like to hear your opinions nonetheless.

Cheers,
Tom

BTW If you would like to look at details of my implementation you can find them here:
http://github.com/Gozala/light-traits/blob/master/lib/traits.js

Thanks!

Cheers,
Tom
Reply all
Reply to author
Forward
0 new messages