Tutorial

This tutorial explains how to use the traits.js library by means of some concrete examples. This tutorial assumes a basic understanding of Javascript and of object-oriented programming in general. It does not assume detailed knowledge of traits in other languages, although this tutorial won't discuss their background, which is explained elsewhere.

Part 1: An Enumerable Trait

Say we want to define a reusable piece of code for enumerating an abstract sequence of elements. Sure, the latest version of Javascript defines a number of useful higher-order operations on Array.prototype, such as map, filter and reduce operations. But these are defined only on arrays. If we define our own datatype, we'll have to redefine all of these operations from scratch.

Traits are reusable building blocks. Their goal is to factor out a common piece of functionality that can be reused by multiple abstractions, regardless of those abstractions' inheritance chain (in Javascript: prototype chain). In traits.js, a trait is simply a collection of required and provided properties. Required properties are like abstract methods in a traditional class hierarchy: they must be implemented by the trait composer. The trait's provided methods may rely on these required methods.

Consider a simple EnumerableTrait that provides the higher-order methods map, filter and reduce given a forEach method that can provide it with successive elements of a sequence:

var EnumerableTrait = Trait({
  // the trait requires these properties
  forEach: Trait.required,

  // the trait provides these properties:
  map: function(fun) {
    var seq = [];
    this.forEach(function(e,i) {
      seq.push(fun(e,i));
    });
    return seq;
  },
  filter: function(pred) {
    var seq = [];
    this.forEach(function(e,i) {
      if (pred(e,i)) {
        seq.push(e);
      }
    });
    return seq;
  },
  reduce: function(init, fun) {
    var result = init;
    this.forEach(function(e,i) {
      result = fun(result, e, i);
    });
    return result;
  }
});

A trait is defined by invoking the Trait constructor. This constructor takes as its argument a simple Javascript object literal that describes the properties of the trait. Required properties are defined by assigning a property name to a distinguished Trait.required value.

Note that the implementation of map, filter and reduce invokes the required forEach method by means of a message sent to this. In the context of a trait, this refers to an instance object that will be composed from one or more traits. It is expected that this eventually defines all required methods. In part 2, we'll see how this is ensured.

You may have noticed that map and filter both return arrays, regardless of the type of abstract sequence to which they are applied. They don't return a mapped or filtered version of the original datatype. This can be fixed by parameterizing the trait explicitly with a constructor for the kind of sequence to which the trait is applied. Have a look at the completed example code to see how such a parameterized trait could be constructed.

Part 2: An Enumerable Interval

Now that we have defined a reusable EnumerableTrait abstraction, let's define a concrete sequence datatype that makes use of it. Consider an integer interval bounded by a lower bound min (inclusive) and an upper bound max (exclusive). We will use the notation min..!max to denote such an interval. An interval is enumerable as a sequence of integers from min up to and including max - 1. Let's define a function makeInterval(min, max) that produces instances of such a data type:

function makeInterval(min, max) {
  return Trait.create(Object.prototype,
    Trait.compose(
      EnumerableTrait,
      Trait({
        start: min,
        end: max,
        size: max - min - 1,
        toString: function() { return ''+min+'..!'+max; },
        contains: function(e) { return (min <= e) && (e < max); },
        forEach: function(consumer) {
          for (var i = min; i < max; i++) {
            consumer(i,i-min);
          }
        }
      })));
}

An interval object is defined as an instance of a trait (since it is created by calling Trait.create). But an instance of what trait, exactly? It is an instance of a composite trait: a combination of the EnumerableTrait defined in part 1, and an anonymous trait describing the properties specific to an interval data type. The anonymous trait is created by calling the Trait constructor. The two traits are combined into a composite trait by means of Trait.compose.

Recall from part 1 that EnumerableTrait provides map, reduce and filter but requires the composer to provide it with a forEach method to enumerate the sequence's elements. The interval data type adheres to this contract. For this concrete interval sequence, forEach just yields the consecutive integers in the range min..!max. The interval data type benefits from this contract: by providing this one forEach method, it gets map, reduce and filter for free:

var i = makeInterval(0,5);
i.start // 0
i.end // 5
i.reduce(0, function(a,b) { return a+b; }) // 0+1+2+3+4 = 10

The prototype of the interval object is set to Object.prototype. If the interval object is supposed to be part of some collection hierarchy, we could have also made it inherit from Collection.prototype. What's important here is that the prototype delegation hierarchy of the instance is completely separate from the trait composition. Both are independent of one another.

Part 3: A Comparable Trait

Let's define a second reusable trait. A comparable data type is a data type that can be compared with instances of itself using various boolean equality and inequality operators. Since most of these boolean operators can be defined in terms of others, it makes sense to factor out the common behavior in a trait:

var ComparableTrait = Trait({
  '<': Trait.required, // this['<'](other) -> boolean
 '==': Trait.required, // this['=='](other) -> boolean

 '<=': function(other) {
    return this['<'](other) || this['=='](other);
  },
  '>': function(other) {
    return other['<'](this);
  },
 '>=': function(other) {
    return other['<'](this) || this['=='](other);
  },
 '!=': function(other) {
    return !(this['=='](other));
  }
});

This trait provides four relational operators whose implementation depends on two required relational operators. By providing an implementation of these two operators, a concrete comparable data type "inherits" the implementation of the other four operators for free.

Part 4: An Enumerable Comparable Interval

Recall our bounded integer interval data type from part 2. We can define what it means for two intervals to be comparable. An interval a..!b == c..!d if and only if a == c && b == d. One possible way to define less-than for intervals is that a..!b < c..!d if and only if b <= c, i.e. if the upper bound of the first interval does not exceed the lower bound of the second interval. For the mathematically inclined: this definition only induces a partial order on intervals (i.e. there exist overlapping intervals i1 and i2 such that neither i1 < i2 nor i2 < i1) but that's OK.

Having defined what it means for intervals to be comparable, we can go ahead and reuse the ComparableTrait defined in part 3:

function makeInterval(min, max) {
  return Trait.create(Object.prototype,
    Trait.compose(
      EnumerableTrait,
      ComparableTrait,
      Trait({
        start: min,
        end: max,
        size: max - min - 1,
        toString: function() { return ''+min+'..!'+max; },
        '<': function(ival) { return max <= ival.start; },
        '==': function(ival) { return min == ival.start && max == ival.end; },
        contains: function(e) { return (min <= e) && (e < max); },
        forEach: function(consumer) {
          for (var i = min; i < max; i++) {
            consumer(i,i-min);
          }
        }
      })));
}

Trait.compose can combine any number of traits. The resulting interval abstraction now reuses two traits, and is therefore both enumerable and comparable. Again, in order to adhere to EnumerableTrait's contract, the interval trait must provide implementations for < and ==. If any of these implementations were missing, Trait.create would throw an exception. Now we can happily compare intervals as well:

var i1 = makeInterval(0,5);
var i2 = makeInterval(7,12);
i1['=='](i2) // false
i1['<'](i2) // true

Part 5: Dealing With Conflicts

Thus far, we have not yet encountered a situation where the composition of two or more traits leads to a name clash. The EnumerableTrait, ComparableTrait and interval trait all define disjoint properties, so their composition does not produce conflicts. Now consider what happens if the author of the EnumerableTrait adds a method named contains that, given an element, returns whether or not the element is part of the sequence. It has a very naive implementation (we could speed it up, but it wouldn't add anything new to the discussion):

var EnumerableTrait = Trait({
  forEach: Trait.required,
  // map, filter, reduce defined as above
  contains: function(e) {
    var result = this.filter(function (elt) { return elt === e; });
    return result.length > 0;
  }
});

If we were to run our code again after this change, the code would break. Why? When EnumerableTrait and the anonymous interval trait are combined using Trait.compose, this function will notice that both traits define a method named contains. It records this fact in its resulting composite trait by binding contains to a conflicting property. Later, when Trait.create instantiates this composite trait, it notices the unresolved conflict, and throws an exception.

This behavior is perhaps the biggest advantage of traits over mixins or traditional multiple inheritance strategies: the fact that name clashes are not resolved implicitly, but are rather recorded as conflicts that must be explicitly resolved by the trait composer. Note that the occurrence of the conflict is independent of the order in which the traits are composed. Any other ordering of the arguments to Trait.compose would have resulted in the same conflict. This is a Good Thing.

So how do we resolve this conflict? In this particular case, the designer of the interval data type will prefer his own, substantially faster, implementation of contains rather than inheriting a generic version from EnumerableTrait. The conflict can be resolved by the trait composer by explicitly excluding the contains property from EnumerableTrait before composing it. The tool for resolving conflicts in traits.js is named Trait.resolve:

function makeInterval(min, max) {
  return Trait.create(Object.prototype,
    Trait.compose(
      Trait.resolve({ contains: undefined }, EnumerableTrait),
      ComparableTrait,
      Trait({
        // interval implementation, as in part 4
      })));
}

Rather than directly reusing EnumerableTrait, the interval implementor reuses a resolved trait that is in every way identical to EnumerableTrait, except that contains is "undefined". What this means is that, in the resolved trait, the property contains is marked as a required property, rather than as a provided property. When Trait.compose subsequently composes this trait with the interval trait, it will replace this required property with the implementation provided by the interval trait.

Why does Trait.resolve turn contains into a required property rather than just removing it entirely from the trait? It's always possible (although traits.js makes no attempt to verify this) that other methods defined by EnumerableTrait depend on the implementation of the excluded property. In order to maintain the integrity of the trait, we have to express the fact the composer should provide EnumerableTrait with another implementation of the excluded property. This is done by turning the excluded property into a required property of the trait.

Sometimes a composer only wants to augment the definition of a method inherited from a trait. That is, the composer wants to perform some behavior specific to the data type, then perform the generic behavior inherited from the trait. The composer must somehow be able to refer to the generic version of the method. Simply excluding the method from the trait disables this. To this end, Trait.resolve also allows one to rename properties:

var t2 = Trait.resolve({ oldName: 'newName' }, t1);

In this case, t2.newName will refer to whatever t1.oldName refers. However, all references to oldName in the trait's methods are unaffected: they are not renamed to newName. Just as in the case of excluded properties, t2 will record the fact that one of its properties was renamed by defining oldName as a required property: since other methods of t1 may continue to depend upon the existence of oldName, a composer of t2 must provide an implementation of oldName. However, when the composer now defines his own version of oldName, he or she can refer to the original (generic) implementation inherited from the trait by means of the name newName.

Resolving conflicts through renaming is analogous to method overriding in classical single-inheritance. Think of overriding a method m in a subclass as 'renaming' the superclass method to super.m.

Conclusion

Now that you know how to deal with conflicts, you're all set to start using traits.js. There really isn't that much more to it than this. There are a couple of functions provided by the library that were not covered in this tutorial, most notably Trait.override and Trait.object which are mainly convenience functions and Trait.eqv which can be regarded as an equality operator for traits.

A complete version of the source code examples discussed in this tutorial can be downloaded here.

If you're interested in the data format used by traits.js to represent traits, it is explained in some detail elsewhere. Briefly, traits are represented not as opaque values, but as plain property descriptor maps. This is the same format as accepted by the Ecmascript 5 built-in function Object.create. For this reason, property descriptor maps and traits are completely interchangeable. If anything, it means you can use traits.js as a simple library for manipulating arbitrary property descriptor maps.

Read more about traits.js in the howtonode.org article on traits.js.

Bookmark and Share