Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 261 lines (186 sloc) 19.136 kb
2a3406a @tvcutsem added article about traits.js
tvcutsem authored
1 Title: Creating safe and composable 'mixins' with traits.js
2 Author: Tom Van Cutsem
5eb52c4 @creationix Update date and publish.
authored
3 Date: Wed Nov 10 2010 10:33:29 GMT-0800 (PST)
2a3406a @tvcutsem added article about traits.js
tvcutsem authored
4
5 In this article I will introduce [_traits.js_](http://traitsjs.org), a small library to define, compose and instantiate traits. Traits are reusable sets of properties and form an alternative to multiple inheritance or mixins.
6
7 ## Traits for Javascript
8
9 A common pattern in Javascript is to add ("mixin") the properties of one object to another object. _traits.js_ provides a few simple functions for performing this pattern safely as it will detect and report conflicts (name clashes) created during a composition. Also, a trait can specify that it can only be added to an object that defines certain required properties, and will fail to compose if these requirements are not satisfied.
10
11 There exist many libraries that add trait support to Javascript in one way or another. What makes _traits.js_ different?
12
13 - It is minimal. _traits.js_ introduces just a handful of methods to create, combine and instantiate traits. Moreover, it doesn't try to introduce the concept of a "class" in Javascript. _traits.js_ reuses Javascript functions for the roles traditionally attributed to classes. A class is just a function that returns new trait instances.
14 - It reuses and extends the [property descriptor](http://ejohn.org/blog/ecmascript-5-objects-and-properties/) format, introduced in ECMAScript 5th edition for describing objects using `Object.create`, as the format for representing traits. This has two implications: first, it means _traits.js_ traits can be used as an argument to ES5 built-ins such as `Object.create`. Second, it means _traits.js_'s own functions, described later, can operate on standard ES5 object descriptions, as composed from the return value of built-ins such as `Object.getOwnPropertyDescriptor`.
15 - It embraces a functional programming style: the core of _traits.js_ consists of a handful of "trait combinator" functions, which take traits as their argument and return new traits. These combinators are pure functions: they have no side-effects and do not modify their argument values, instead producing fresh traits upon each invocation. You can compose these functions freely without fear of unanticipated side-effects.
16
17 ### Getting started
18
19 _traits.js_ is available as a node package called "traits" via npm. A simple `npm install traits` should make it available in node.js. Then load it up as follows:
20
21 var Trait = require('traits').Trait;
22
23 This creates a local copy of the library's single exported variable, `Trait`. Evaluating `Trait` in the shell reveals the library's entire API:
24
25 { [Function: Trait]
26 required: { toString: [Function] }
27 , compose: [Function: compose]
28 , resolve: [Function: resolve]
29 , override: [Function: override]
30 , create: [Function: create]
31 , eqv: [Function: eqv]
32 , object: [Function: object]
33 }
34
35 ### Trait creation
36
37 As you can see from the above printout, `Trait` is a function. Calling it creates new traits. Here's a simple trait that abstracts equality (I will be using a slightly adapted version of the running example from the [original traits paper](http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf)):
38
39 var TEquality = Trait({
40 equals: Trait.required,
41 differs: function(x) { return !this.equals(x); }
42 });
43
44 By convention, we usually prefix traits with a capital T to distinguish them from regular Javascript constructor functions. Traits may _require_ and _provide_ a set of properties. Provided properties are simply those properties that will be mixed into an object using the trait. Required properties are those that a trait expects to be provided by its "client" (the object that uses it). In _traits.js_, required properties are defined by binding the property name to the singleton value `Trait.required`.
45
46 `TEquality` provides a `differs` property to and requires an `equals` property from its client. Note that `differs` is implemented in terms of `equals`, and that it assumes that `this` has an implementation for it. This should all be fairly familiar to any object-oriented programmer. To relate traits to more traditional OOP concepts, it is not far wrong to think of a trait as an abstract class, and to think of its required properties as "abstract" properties, to be provided by a "subclass".
47
48 ### Composing traits
49
50 The workhorse of the _traits.js_ library is a function called `Trait.compose`. This function takes any number of traits as an argument and returns a single, fresh, "composite" trait that contains all of the properties of its arguments. Consider the following trait:
51
52 var TMagnitude = Trait.compose(TEquality, Trait({
53 smaller: Trait.required,
54 greater: function(x) { return !this.smaller(x) && this.differs(x) },
55 between: function(min, max) {
56 return min.smaller(this) && this.smaller(max);
57 }
58 }));
59
60 Give `TMagnitude` a concrete implementation for `smaller` and it will provide an implementation for the methods `greater` and `between`. Actually, `TMagnitude` is defined as a composite trait: it combines the properties of `TEquality` with those of an anonymous nested trait. This means that `TMagnitude` actually has two required properties: `smaller` and `equals`, and that it has three provided properties: `greater`, `between` and `differs`:
61
62 <img width="100%" src="/traitsjs/1-TMagnitude.png" title="TMagnitude" alt="Composition of TMagnitude"/>
63
64 Let's compose `TEquality` and `TMagnitude` further into a `TCircle` trait that captures generic circle behavior:
65
66 function TCircle(center, radius) {
67 return Trait.compose(
68 TMagnitude,
69 TEquality,
70 Trait({
71 center: center,
72 radius: radius,
73 area: function() { return Math.PI * this.radius * this.radius; },
74 equals: function(c) { return c.center === this.center &&
75 r.radius === this.radius },
76 smaller: function(c) { return this.radius < c.radius }
77 }));
78 }
79
80 There are a couple of things going on here:
81
82 - `TCircle` is not defined as a singleton bound to a `var` but rather as a function. `TCircle` is in fact a trait generator: call it and you will get a new trait. By turning `TCircle` into a function, it can be parameterised with state, in this case the `center` and `radius` of the circle. The general rule is simple: if your trait is stateless, define it as a singleton object. If your trait is stateful, define it as a function.
83 - Like `TMagnitude`, `TCircle` is a composite trait, composed from the two traits we defined earlier, and a nested anonymous trait that adds the circle-specific behaviour. By composing `TEquality` and `TMagnitude`, circle objects created by this trait will be comparable using methods like `differs` and `greater`.
84 - `TCircle` provides an implementation for the methods required by `TMagnitude` and `TEquality`, such that `TCircle` will only provide and not require any properties.
85 - Even though `TMagnitude` also uses `TEquality`, the duplicated use of `TEquality` in `TCircle` does not cause any problems: _traits.js_ detects that the same trait is being composed and ignores the duplicated composition.
86
87 The following picture illustrates the composition of `TCircle`:
88
89 <img width="100%" src="/traitsjs/2-TCircle.png" title="TCircle" alt="Composition of TCircle"/>
90
91 Although this simple example doesn't do justice to it, here's the hidden power of `Trait.compose`: the ordering of its arguments _does not matter_. No matter in what order the argument traits are specified, `Trait.compose` will return an equivalent trait in all cases. For the mathematically inclined: `Trait.compose` is a commutative operator, like addition, e.g. `a + b = b + a`. Similarly, when using multiple nested calls to `Trait.compose`, it doesn't matter how the calls are nested. For the mathematically inclined: `Trait.compose` is an associative operator, like addition, e.g. `(a + b) + c = a + (b + c)`.
92
93 These properties sound like "nice to have" from a mathematical point of view, but they are actually crucial from a software engineering point of view: thanks to this commutativity and associativity, we as programmers don't need to understand in what order a trait was composed from its subparts, even in the case of a very complicated trait that involves a deep "hierarchy" of subtraits, possibly spread out over different files. It makes trait composition much more declarative than multiple inheritance, which requires you to do a mental graph traversal to figure out the relative interdependencies and priorities between the different superclass methods. Trait composition, at each level, "merges" the component parts into a single, larger, composite trait. All of the methods of all subparts have equal priority. But hold on, what if multiple traits define a property with the same name?
94
95 ### Conflicts!
96
97 Assume we want to make our circles a bit more colorful and decide to mixin a color trait:
98
99 function TColor(rgb) {
100 return Trait.compose(TEquality, Trait({
101 get rgb() { return rgb; },
102 equals: function(col) { return col.rgb.equals(this.rgb); }
103 }));
104 }
105
106 `TColor`, like `TCircle`, is a "stateful" trait (i.e. it is defined as a function that can take parameters to capture state). We can imagine a color trait providing much more functionality to manipulate the RGB color, but for the sake of brevity the color trait provides just a simple accessor for the RGB value. `TColor` also reuses `TEquality` and defines `equals` in terms of equal RGB color values. Now, the definition of `TCircle` is modified to additionally reuse `TColor`:
107
108 function TCircle(center, radius, rgb) {
109 return Trait.compose(
110 TMagnitude,
111 TEquality,
112 TColor(rgb),
113 Trait({
114 center: center,
115 radius: radius,
116 area: function() { return Math.PI * this.radius * this.radius; },
117 equals: function(c) { return c.center === this.center &&
118 r.radius === this.radius },
119 smaller: function(c) { return this.radius < c.radius }
120 }));
121 }
122
123 Both `TColor` and `TCircle` provide an `equals` method. Which one will get invoked on an instance of `TCircle`? The answer is: neither one. When `Trait.compose` detects that two or more of its argument traits define a property with the same name, it records this "conflict" by defining a special "conflicting property" in the resulting trait. No exception is thrown at this stage (that will only happen if a trait containing a conflict is instantiated, as explained later). The resulting trait will contain a "conflicting property" but may still be composed further with other traits, as shown below:
124
125 <img width="100%" src="/traitsjs/3-Conflicts.png" title="Conflicts" alt="Conflicts in composition of TCircle"/>
126
127 ### Three ways to resolve a conflict
128
129 Once we have detected a conflict, we will probably want to refactor the code to resolve it. The philosophy of traits is that it is the job of the *composer* to resolve conflicts. There are three ways in which the composer can do so:
130
131 1. by renaming the conflicting property name in one of the conflicting traits.
132 2. by excluding the property name altogether from one of the conflicting traits.
133 3. by explicitly overriding the properties of one trait with those of another trait.
134
135 The first two alternatives can be accomplished using the function `Trait.resolve`. Here's how one can resolve the conflict through renaming:
136
137 function TCircle(center, radius, rgb) {
138 return Trait.compose(
139 TMagnitude,
140 TEquality,
141 Trait.resolve({ equals: 'equalColors' }, TColor(rgb)),
142 Trait({
143 center: center,
144 radius: radius,
145 area: function() { return Math.PI * this.radius * this.radius; },
146 equals: function(c) { return c.center === this.center &&
147 r.radius === this.radius },
148 smaller: function(c) { return this.radius < c.radius }
149 }));
150 }
151
152 The call `Trait.resolve({ a: 'b' }, t)` returns a trait that is equivalent to `t` but with `t.a` bound to `t.b` instead. In the above example, we've renamed the `equals` method provided by `TColor` to `equalColors`. This renamed trait is then composed with the other traits, producing a conflict-free `TCircle` trait, as shown below:
153
154 <img width="100%" src="/traitsjs/4-Renaming.png" title="Renaming" alt="Renaming"/>
155
156 The second alternative is to exclude a conflicting property, like so:
157
158 function TCircle(center, radius, rgb) {
159 return Trait.compose(
160 TMagnitude,
161 TEquality,
162 Trait.resolve({ equals: undefined }, TColor(rgb)),
163 Trait({
164 center: center,
165 radius: radius,
166 area: function() { return Math.PI * this.radius * this.radius; },
167 equals: function(c) { return c.center === this.center &&
168 r.radius === this.radius },
169 smaller: function(c) { return this.radius < c.radius }
170 }));
171 }
172
173 The call `Trait.resolve({ a: undefined }, t)` will return a trait equivalent to `t` with `a` turned into a required property:
174
175 <img width="100%" src="/traitsjs/5-Excluding.png" title="Excluding" alt="Excluding"/>
176
177 The third alternative is for the composer to explicitly specify that one of the traits overrides the properties of another trait:
178
179 function TCircle(center, radius, rgb) {
180 return Trait.compose(
181 TMagnitude,
182 TEquality,
183 Trait.override(
184 Trait({
185 center: center,
186 radius: radius,
187 area: function() { return Math.PI * this.radius * this.radius; },
188 equals: function(c) { return c.center === this.center &&
189 r.radius === this.radius },
190 smaller: function(c) { return this.radius < c.radius }
191 }),
192 TColor(rgb)));
193 }
194
195 The anonymous trait and `TColor` are now composed using `Trait.override` instead of `Trait.compose`. Because of this, the `equals` method of the anonymous trait will take precedence over the `equals` method of `TColor`:
196
197 <img width="100%" src="/traitsjs/6-Overriding.png" title="Overriding" alt="Overriding"/>
198
199 Note that the order of arguments to `Trait.override` matters (left-to-right priority), which is why the two traits have been reordered compared to the previous examples. This also exposes a significant drawback of `Trait.override` compared to `Trait.compose`: it's not commutative, so you'll have to pay closer attention to the ordering of things! `Trait.override` is very similar to "standard" inheritance (with the subclass's methods implicitly overriding the superclass's methods).
200
201 ### Trait instantiation
202
203 Traits can be instantiated into objects using the function `Trait.create`:
204
205 function Circle(center, radius, rgb) {
206 return Trait.create(Object.prototype,
207 TCircle(center, radius, rgb));
208 }
209
210 The first argument to `Trait.create` is the _prototype_ of the trait instance. `Trait.create` is modelled after the new ES5 built-in `Object.create`, which also takes the object's prototype as its first argument. In fact, it's possible to use `Object.create` to instantiate traits as well:
211
212 function Circle(center, radius, rgb) {
213 return Object.create(Object.prototype,
214 TCircle(center, radius, rgb));
215 }
216
217 <img width="100%" src="/traitsjs/7-Create.png" title="Create" alt="Instantiating traits"/>
218
219 Now we can start creating and using circle objects:
220
221 var c1 = Circle(new Point(0,0), 1, new Color(255,0,0));
222 var c2 = Circle(new Point(0,0), 2, new Color(255,0,0));
223 c1.smaller(c2) // true
224 c1.differs(c2) // true
225
226 `Object.create` is provided as a built-in in an ES5 engine. On ES3 engines, _traits.js_ defines it. Next, let's look at how instantiating traits using `Trait.create` and `Object.create` differ.
227
228 #### Using Trait.create
229
230 When instantiating a trait, `Trait.create` performs two "conformance checks":
231
232 - If the trait still contains required properties, and those properties are not provided by the specified prototype, `Trait.create` throws. This situation is analogous to trying to instantiate an abstract class.
233 - If the trait still contains conflicting properties, `Trait.create` also throws.
234
235 In addition, _traits.js_ ensures that the new trait instance has high integrity:
236
237 - The `this` of all trait methods is bound to the new instance. This means you can safely select methods from a trait instance and pass them around as functions, without fear of accidentally binding `this` to the global object.
238 - In an ES5 engine, the instance is created as a _frozen_ object: clients cannot add, delete or assign to the instance's properties.
239
240 #### Using Object.create
241
242 Since `Object.create` is an ES5 built-in that knows nothing about traits, it will not perform the above trait conformance checks and will not fail on incomplete or inconsistent traits. Instead, required and conflicting properties are treated as follows:
243
244 - Required properties will be bound to `undefined`, and will be non-enumerable (i.e. they won't show up in `for-in` loops on the trait instance). This makes them virtually invisible. Clients can still assign a value to these properties later.
245 - Conflicting properties have a getter and a setter that throws when accessed. Hence, the moment a program touches a conflicting property, it will fail, revealing the unresolved conflict.
246
247 `Object.create` does not bind `this` and does not generate frozen instances. Hence, the new trait instance can still be modified by clients.
248
249 It's up to you as a programmer to decide which instantiation method, `Trait.create` or `Object.create` is more appropriate: `Trait.create` fails on incomplete or inconsistent traits and generates frozen objects, `Object.create` may generate incomplete or inconsistent objects, but as long as a program never actually touches a conflicting property, it will work fine (which fits with the dynamically typed nature of Javascript).
250
251 ## Conclusion
252
253 In the introduction I mentioned that _traits.js_ is minimal. All in all, you only need to know four functions to work with the library:
254
255 - Use `Trait({...})` to construct a new trait.
256 - Use `Trait.compose` to compose smaller traits into larger ones.
257 - Use `Trait.resolve` to create a trait with renamed or excluded properties, in order to avoid conflicts and disambiguate property names.
258 - Use `Trait.create(prototype, trait)` to instantiate a trait into a new object. If you require the trait instance to remain extensible, use `Object.create` instead.
259
5eb52c4 @creationix Update date and publish.
authored
260 That's it. There isn't much more to it. The complete API and another tutorial can be found on the [_traits.js_ home page](http://traitsjs.org). If you want to peek under the hood of the library and know more about the format in which traits are represented, [this page](http://code.google.com/p/es-lab/wiki/Traits#Traits_as_Property_Maps) provides all the details.
Something went wrong with that request. Please try again.