Simpler API for creating traits

21 views
Skip to first unread message

gozala

unread,
Sep 17, 2010, 6:40:52 AM9/17/10
to traits.js
Hi Everyone,

First of all thank Tom, Mark for a great work!

We at Mozilla Jetpack team have borrowed your API design and have
implement almost compatible traits library.
I wanted to describe here changes we made to the API, explain
reasoning and get some opinions from you. I would also encourage
adobting those unless there are reasons for not to. I can also
contribute patch in case you'll decide to adopt those changes.

So here is my small list:

1. `Trait.create` instantiates objects that have all the methods
bounded to the instance. It undoubtedly a nice thing to have on one
hand but:

- It is suboptimal in terms of object instantiation speed and memory
footprint.
- Bring somewhat different ideology to already available in ES.
- Makes mixing traits with prototypical inheritance unintuitive.

In our implementation methods / getters / setters are not bounded to
the instance, neither instance is frozen. We do it since we want to
encourage minimal public APIs. To achieve somewhat same result as in
traits.js we implement [special trait](http://gist.github.com/583979)
that has a lazy property that generates frozen proxy object to the
public API of the instance. It also provides advantage of binding only
a subset of methods per instance. In example it looks like:

const MyTrait = Trait.compose(PublicTrait
, Trait(
{
, _public: Trait.required
, _privateMethod: function() {
....
}
, publicMethod: function() {
....
}
})
)


function MyClass() {
return Trait.create(
{ _secret: 'my secret'
, MyTrait
})._public
}

let myObject = MyClass();
'_secret' in myObject // false
'_privateMethod' in myObject // false
'publicMethod' in myObject // true

2. Everytime you need to create an instance from trait you Have to
type Trait.create(proto, MyTrait) but as non of us likes typing more
then have to we took slightly different approach, instead of copying
all the property descriptors to the blank map ({}) we do copy them to
the Object.create(Trait.prototype) where Trait.prototype has two
methods defined:

Trait.prototype.toString = function toString() {
return '[object ' + this.constructor.name + ']'
}
Trait.prototype.create = function create(proto) {
return Trait.create(undefined == proto ? {} : proto, this)
}
Trait.prototype.resolve = function resolve(resolutions) {
return Trait.resolve(resolutions, this)
}
This gives us some syntax sugar:

const Sugar = Trait({
....
})

Sugar.create()
Sugar.create(Ancestor.prototype)

3. Another place where we added some more syntax sugar is in
Trait.compose function. While in original implementation it explicitly
requires Traits to be passed to it we allow passing proprerty maps as
well to create Traits out of it. In fact only way to create a trait is
actuall `Trait.compose`. It's pretty easy to distinguish weather or
not argument is trait or property map due to change described in
previous paragraph and simple statement: `arguments[i] instanceof
Trait`. So with this change in sugared trait can be written as
follows:

const MoreSugare = Trait.compose(Sugar
, MyTrait
, AnotherTrait
, { name: 'anonymus trait'
, foo: 'bar'
}
) 4. Changes described in the previous paragraph frees up some
space for those Trait function for a rare cases where we need to
create traits with non-enumerable properties for example. In our case
Trait function takes property descriptors map and creates trait out of
it. All this changes together allows us to write traits as follows:


const FooTrait = Trait.compose(
{ _fooSecret: 'secret'
, fooMethod: function() {
.....
}
, conflict: '##'
, .....
})
const BarTrait = Trait(
{ bar: { value: 'bar', enumerable: false }
, conflict: { ... }
, .....
})

const BlaTrait = Trait.compose(
FooTrait.resolve({ conflict: null }),
BarTrait,
PublicTrait,
{ blaMethod: function() {
....
}
, blaProperty: 90
, ....
}
)

function Bla(x, y) {
return BlaTrait.create(
{ constructor: Bla,
, x: x,
, y: y,
})._public
}


That's it. I'm actually thinking of a ways to merge Trait function
with Trait.compose or of swapping them around but have not done it so
far. Hope to hear your feedback!!

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

Mark S. Miller

unread,
Sep 17, 2010, 2:36:01 PM9/17/10
to trai...@googlegroups.com
Hi Irakli,

Before getting into the specifics, I'd like ask, if you had <http://wiki.ecmascript.org/doku.php?id=strawman:syntax_for_efficient_traits> and it were efficient, would that be adequate to address your concerns?
--
    Cheers,
    --MarkM

Tom Van Cutsem

unread,
Sep 18, 2010, 2:36:01 PM9/18/10
to trai...@googlegroups.com
2010/9/17 gozala <rfo...@gmail.com>:

> Hi Everyone,
>
> First of all thank Tom, Mark for a great work!

It's our pleasure, thanks :-)

>
> We at Mozilla Jetpack team have borrowed your API design and have
> implement almost compatible traits library.
> I wanted to describe here  changes we made to the API, explain
> reasoning and get some opinions from you. I would also encourage
> adobting those unless there are reasons for not to. I can also
> contribute patch in case you'll decide to adopt those changes.
>
> So here is my small list:
>
> 1. `Trait.create` instantiates objects that have all the methods
> bounded to the instance. It undoubtedly a nice thing to have on one
> hand but:
>
> - It is suboptimal in terms of object instantiation speed and memory
> footprint.
> - Bring somewhat different ideology to already available in ES.
> - Makes mixing traits with prototypical inheritance unintuitive.

I understand your points, others have raised similar issues on the
es-discuss list recently. Of course, you could use Object.create to
create unbound trait instances, but then you would not get the early
error checking for missing required and for conflicting properties. I
feel tempted to add a third method (next to Object.create and
Trait.create) that returns unbound instances but does do the necessary
trait conformance checks, although I think 3 creation methods is too
much. Another option could be for traits.js to 'monkey-patch'
Object.create to perform the trait conformance checks before creating
the object. I'm ambivalent about that approach since modifying
existing primitives should not be done lightly.

Clever. I do wonder: since you create a public 'facade' proxy, there
may be issues with identity tests. That is, the facade proxy is not
=== to the hidden trait instance. Also, if the hidden trait instance
ever returns 'this' from one of its methods, clients could still get a
direct reference to it.

>
> 2. Everytime you need to create an instance from trait you Have to
> type Trait.create(proto, MyTrait) but as non of us likes typing more
> then have to we took slightly different approach, instead of copying
> all the property descriptors to the blank map ({}) we do copy them to
> the Object.create(Trait.prototype) where Trait.prototype has two
> methods defined:
>
>    Trait.prototype.toString = function toString() {
>     return '[object ' + this.constructor.name + ']'
>    }
>    Trait.prototype.create = function create(proto) {
>      return Trait.create(undefined == proto ? {} : proto, this)
>    }
>    Trait.prototype.resolve = function resolve(resolutions) {
>      return Trait.resolve(resolutions, this)
>    }
> This gives us some syntax sugar:
>
>    const Sugar = Trait({
>      ....
>    })
>
>    Sugar.create()
>    Sugar.create(Ancestor.prototype)

Indeed, this syntax is much nicer. In an earlier design, I had
considered a similar syntax: create, compose, resolve, etc. were
methods defined on traits. However, the main reason we changed this
was to clearly separate the namespace of trait operations from the
namespace of the trait object being described. For example, in your
code, a bug could arise if one of your traits accidentally defines a
method named `create` or `resolve`. For example, what happens if we
define:

const Cookie = Trait.compose({ create: function() { return 'cookie
created'; } }); // using your semantics of Trait.compose
...
Cookie.create(); // this would unexpectedly access the Cookie's
'create' property rather than call Trait.prototype.create.

>
> 3. Another place where we added some more syntax sugar is in
> Trait.compose function. While in original implementation it explicitly
> requires Traits to be passed to it we allow passing proprerty maps as
> well to create Traits out of it. In fact only way to create a trait is
> actuall `Trait.compose`. It's pretty easy to distinguish weather or
> not argument is trait or property map due to change described in
> previous paragraph and simple statement: `arguments[i] instanceof
> Trait`. So with this change in sugared trait can be written as
> follows:
>
>     const MoreSugare = Trait.compose(Sugar
>    , MyTrait
>    , AnotherTrait
>    , { name: 'anonymus trait'
>      , foo: 'bar'
>      }
>    )

That's useful. In my implementation, since trait values really are
just property descriptor maps, I can't distinguish normal objects from
traits.
Perhaps I could make it work if I also create trait objects as
Object.create(Trait.prototype) instead of as normal empty objects. As
long as Trait.prototype is an empty object, there can be no confusion
of namespaces as in point 2) above.

Ok, I see. Because in your version, "Trait.compose({...})" is
essentially the same as "Trait({...})" in my version, you can use the
Trait constructor for turning property descriptor maps into traits.
Since property descriptor maps _are_ traits in my version, I don't
have a need for converting them. I guess your Trait constructor needs
to make sure that the property descriptor map now inherits from
Trait.prototype to inherit `create` and `resolve`.

>
> That's it. I'm actually thinking of a ways to merge Trait function
> with Trait.compose or of swapping them around but have not done it so
> far. Hope to hear your feedback!!

I understand the reason behind the changes you made, and all in all it
probably makes the library more lightweight to use. The fundamental
problem in the design I think, is the confusion of namespaces so that
you can't really create trait objects with methods named `create` and
`resolve`. You have to think of them as special 'keywords', even
though they are not. While I think in practice this will hardly ever
be a real problem, it still bothers me :-) One solution could be to
have Trait.compose check whether the trait defines properties named
`create` or `resolve` and to disallow this, but that would add yet
more runtime costs to trait creation.

There is a way to keep the advantage of defining `create` and
`resolve` as methods on the trait, and to separate the namespaces to
avoid confusion, if you define a "trait" to be a new special value
that e.g. has a pointer to its property descriptor map. But then
traits are no longer the same as normal property descriptor maps. I
really like the fact that in traits.js, a trait is just a property
descriptor map, not a special new kind of object.

That said, I will think about some of the features you presented. I
had not before considered the idea of having trait property descriptor
maps inherit from Trait.prototype or another object, so that we can
actually distinguish "trait" property descriptor maps from other
objects.

Thanks a lot for your feedback!

Cheers,
Tom

Irakli Gozalishvili

unread,
Sep 19, 2010, 6:24:59 AM9/19/10
to trai...@googlegroups.com

Yes I'm also against modifying primitives, besides in some environments it's not going to work cause in our case each module is loaded in independent global scope and primitives are not shared among those.


That is correct, but there is no real need to do === comparison with facade, in fact since it's pretty transparent to the developer that facade is returned it becomes obvious that comparison should happen against this._public. Regarding 2nd point it is true that returning this from public method will expose reference to to a privates but this mistakes are easy to catch in the code reviews.
 

That is also correct and it was bugging me for some time as well, but after thinking of that for a while I came to conclusion that it's not that big of the problem and actually someone might use it to add some hooks on it's trait pre-creation.
 

Well I have similar function like your `trait` with major difference being
function trait(map) {
  if (map instanceof Trait) return map
..... // do what you do in your function
}
 
>
> That's it. I'm actually thinking of a ways to merge Trait function
> with Trait.compose or of swapping them around but have not done it so
> far. Hope to hear your feedback!!

I understand the reason behind the changes you made, and all in all it
probably makes the library more lightweight to use. The fundamental
problem in the design I think, is the confusion of namespaces so that
you can't really create trait objects with methods named `create` and
`resolve`. You have to think of them as special 'keywords', even
though they are not. While I think in practice this will hardly ever
be a real problem, it still bothers me :-) One solution could be to
have Trait.compose check whether the trait defines properties named
`create` or `resolve` and to disallow this, but that would add yet
more runtime costs to trait creation.


As I mentioned before while it can cause some confusion it may also be useful once devs are aware

Trait.compose({
   create: function create(proto) {
      // do something here
      return Trait.prototype.create(somethingElse)
   }
})

Even though I have not found any use of that yet, it may be still useful for example you might make a copy of proto in order to avoid case where changes in proto affect your instance in non ES5 environment.

There is a way to keep the advantage of defining `create` and
`resolve` as methods on the trait, and to separate the namespaces to
avoid confusion, if you define a "trait" to be a new special value
that e.g. has a pointer to its property descriptor map. But then
traits are no longer the same as normal property descriptor maps. I
really like the fact that in traits.js, a trait is just a property
descriptor map, not a special new kind of object.

That said, I will think about some of the features you presented. I
had not before considered the idea of having trait property descriptor
maps inherit from Trait.prototype or another object, so that we can
actually distinguish "trait" property descriptor maps from other
objects.

Thanks a lot for your feedback!

You're more then welcome!
 

Irakli Gozalishvili

unread,
Sep 19, 2010, 6:32:34 AM9/19/10
to trai...@googlegroups.com
Hi Mark,
 
Before getting into the specifics, I'd like ask, if you had <http://wiki.ecmascript.org/doku.php?id=strawman:syntax_for_efficient_traits> and it were efficient, would that be adequate to address your concerns?

I've seen proposal but so far I have a mixed feelings about it, not sure I like it or not but I'm going to speak up on es mailing list once I'll make up my mind about it :)


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


Irakli Gozalishvili

unread,
Sep 19, 2010, 2:05:07 PM9/19/10
to trai...@googlegroups.com

Sorry just realized that I confused things here, cause Trait.compose returns descriptor map so create there will be an object not a function. I do see your concerns regarding this, but don't think it's that big issue
since it's rare case, easy to spot issue and there is always on option to use Trait.prototype.create.call(myTrait, proto) if you really need to have create method.
 

Mark S. Miller

unread,
Sep 19, 2010, 3:47:07 PM9/19/10
to trai...@googlegroups.com
On Sun, Sep 19, 2010 at 3:32 AM, Irakli Gozalishvili <rfo...@gmail.com> wrote:
Hi Mark,
 
Before getting into the specifics, I'd like ask, if you had <http://wiki.ecmascript.org/doku.php?id=strawman:syntax_for_efficient_traits> and it were efficient, would that be adequate to address your concerns?

I've seen proposal but so far I have a mixed feelings about it, not sure I like it or not but I'm going to speak up on es mailing list once I'll make up my mind about it :)

Hi Irakli,

Please don't feel like you need to make up your mind first. Perhaps sharing your mixed feeling before they become unmixed will lead to more insight for all of us. Looking forward to it.



--
    Cheers,
    --MarkM
Reply all
Reply to author
Forward
0 new messages