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