This is an old revision of the document!


Classes with Trait Composition

This is a major revision of the earlier classes and traits strawman in order to reconcile object_initialiser_extensions, especially obj_initialiser_class_abstraction and instance_variables. A prototype implementation of an earlier version of this reconciled strawman is described at Traceur Classes and Traits.

Class Declarations and Expressions

We extend the Declaration production from block scoped bindings to accept a ClassDeclaration. We extend MemberExpression to accept a ClassExpression.

  Declaration :
      LetDeclaration
      ConstDeclaration
      FunctionDeclaration
      ClassDeclaration
  ClassDeclaration :             // by analogy to FunctionDeclaration
      class Identifier Proto? { ClassBody }
      trait Identifier Proto? { ClassBody }
      // alternatives to making "trait" a keyword?
  ExpressionStatement : 
      [lookahead not-in { "{", "function", "const", "class" }] Expression ";"
      // and "trait" if it becomes a new keyword.
  MemberExpression : ...         // "..." means members defined elsewhere
      ClassExpression
  ClassExpression :              // by analogy to FunctionExpression
      class Identifier? Proto? { ClassBody }
      trait Identifier? Proto? { ClassBody }
  ClassBody :                    // by analogy to FunctionBody
      ClassElement*

In a ClassDeclaration or ClassExpression, the class keyword defines an instantiable class whereas the trait keyword(?) defines a non-instantiable class, or trait. Both define a special constructor function to represent that class. For a trait, this constructor function, when called via [[Call]] or [[Construct]] throws a TypeError complaining that this class is non-instantiable.

This constructor function is not a normal function. It cannot be explained simply by local expansion to JavaScript without classes, in order to explain the semi-static errors. Instead, when evaluating a ClassDeclaration or ClassExpression to such a constructor function, if a semi-static error is detected, then a TypeError is thrown then. For a ClassDeclaration, if this error occurs, it is thrown on entry to the enclosing Block, FunctionBody, Program, or ModuleBody. For top-level ClassDeclarations, these errors are therefore effectively early errors – they occur before any code within that Program of Module runs. If a ClassDeclaration or ClassExpression evaluates successfully, i.e., without such semi-static errors, the constructor function is initialized to hold various internal properties to enable later classes dependent on this one to detect their own semi-static errors.

As with a FunctionDeclaration, a ClassDeclaration defines and initializes the named variable at the beginning of the block holding this constructor function. As with FunctionExpressions, a ClassExpression may be anonymous, but if named, defines this name only to be visible within the body of this class, initializing it to be bound to this constructor function.

If the optional Proto is absent, then this constructor function’s “prototype” property is a fresh frozen empty object inheriting directly from Object.prototype. Otherwise, it is initialized according to the Inheritance section below.

Class and Constructor Elements

  ClassElement :                 // by analogy to SourceElement
      Statement                  // but not ReturnStatement
      Declaration                // in scope during rest of ClassBody
      PublicDeclaration          // added as property to *each* class.prototype
      StaticDeclaration          // added as property to *this* class
      MixinElement               // obtain more prototype properties
      Constructor                // at most one
  Constructor :
      new ( FormalParameterList? ) { ConstructorBody }
  ConstructorBody :              // by analogy to FunctionBody
      ConstructorElement*
  ConstructorElement :           // by analogy to SourceElement
      Statement                  // but not ReturnStatement
      Declaration                // in scope during rest of ConstructorBody
      PublicDeclaration          // added as property to *each* instance
      PrivateDeclaration         // added as per-instance state for *this* class
      ProtoChaining              // per-instance state from super-constructor
      MixinChaining              // per-instance state from mixed-in constructor

The body of a class is for initializing class-wide properties once per evaluation of the ClassDefinition or ClassExpression. It consists of ClassElements. These are like the interleaved Declarations and Statements that appear in blocks elsewhere in the language, but with the following differences:

  • As with a Program or ModuleBody, the statements may not include a ReturnStatement.
  • Declarations annotated with “public” also add a same-named property to the constructor function’s “prototype” property. The value of the constructor function’s “prototype” property is not frozen until the ClassBody exits, at which time all such initialization is done.
  • Declarations annotated with “static public” also add a same-named property to the constructor function itself. The constructor function itself is not frozen until the ClassBody exits, at which time all such initialzation is done.
  • MixinElements obtain further prototype properties from the prototype properties of mixed-in classes.
  • A class can have at most one explicit Constructor. An omitted constructor is equivalent to new(){}.

A class’ Constructor describes how this class contributes state towards instances of this class – including instances of classes that inherit from this class and instances of classes that mixin this class. If this class is an instantiable class, i.e., not a trait, then this Constructor also directly describes the [[Call]] and [[Construct]] behavior of the constructor function for this class.

The body of a constructor is for initializing per-instance state for each instance of this class, once per call or chained call to the “new” function. It consists of ConstructorElements. These are like the interleaved Declarations and Statements that appear in blocks elsewhere in the language, but with the following differences:

  • As with a Program or ModuleBody, the statements may not include a ReturnStatement.
  • Declarations annotated with “public” also contribute a same-named property towards the instance. The instance is not frozen until the ConstructorBody exits, at which time all such initialization is done.
  • Declarations annotated with “private” also add a same-named property to an interval [[ClassPrivateState]] property on each instance of a class. These properties represent the class-private instance variables and are accessed using the private(expr) syntax below.
  • ProtoChaining and MixinChaining are explained in the Inheritance section below.
  ExportableDeclaration :
      Declaration
      Identifier = Expression ;
      Identifier ( FormalParameterList? ) { FunctionBody }
      get Identifier ( ) { FuntionBody }
      set Identifier ( FormalParameter ) { FunctionBody }
  PublicDeclaration :
      public ExportableDeclaration
      requires public Identifier ;                          // required data
      requires public Identifier ( FormalParameterList? ) ; // required method
  StaticDeclaration :
      static public ExportableDeclaration
  PrivateDeclaration :
      private ExportableDeclaration

  MemberExpression : ...         // "..." means members defined elsewhere
      private ( AssignmentExpression )

ExportableDeclarations are those which can be annotated in order to export a local declaration as a property of some object. The “Identifier = Expression;” form is just a shorthand for “const Identifier = Expression“. The “Identifier ( FormalParameterList? ) { FunctionBody }” form is just shorthand for the same syntax as preceded by “function“. The get and set forms together define the exported property as an accessor property. Perhaps they also define a like-named variable whose accesses and assignments invoke these same getter and setter functions.

public ExportedDeclaration” exports the declared name as a like-named public property on the object in question. When it appears in a ClassBody, the property is placed onto the class’ constructor’s prototype. When in appears in a ConstructorBody, it is contributed towards being a public property of the instance being constructed, as further determined by classes inheriting or mixing in this one.

requires public ...” declares remaining public members that must be provided by inheritance or composition in order to create an instantiable class, i.e., one that can be declared with the “class” keyword. Such requirements can only appear in a trait.

static public ExportableDeclaration” can only appear in a ClassBody, and initializes a like-named property on the constructor function itself.

private ExportableDeclaration” can only appear in a ConstructorBody, and initializes a class-private instance variable, uniquely associated with the instance being made and visible only from within this class.

private ( AssignmentExpression )” can appear as an expression anywhere within a class where an expression can appear. The closest enclosing class is the containing class. This expression evaluates the AssignmentExpression. If it evaluates to an instance of the containing class, including an instance of a class that inherits or mixes in the containing class, then the expression evaluates to a record of all the class-private instance variables associated with this instance by the containing class. Otherwise, the expression evaluates to undefined.

Inheritance

  Proto :
      : MemberExpression
  MixinElement :
      mixin MemberExpression Renamings? ;
      // alternatives to making "mixin" a keyword?
  ProtoChaining :
      super Arguments Renamings? ;
  MixinChaining
      mixin MemberExpression Arguments Renamings? ;
      // alternatives to making "mixin" a keyword?
  ClassName :
      Identifier
  Renamings :
      Renaming
      Renamings , Renaming
  Renaming                       // to be harmonized with module import rename
      with Identifier as Identifier
      without Identifier

  MemberExpression : ...         // "..." means members defined elsewhere
      super . IdentifierName

Adapted Examples

To facilitate comparisons, the examples below are adapted from earlier or similar proposals. Since some of these are also adapted from each other, you may find some redundancy between these examples.

Adapted from explanations of TraitsJS

Example adapted from http://traitsjs.org/.

  trait ColorTrait {
    new(col) {
      public color() { return col; }
    }
  }
  class Point : (Object) {
    new(x, y) {
      public getX() { return x; }
      public getY() { return y; }
      public toString() { return ''+x+'@'+y; }
    }
  }
  class ColoredPoint {
    mixin ColorTrait;
    mixin Point;
    new(x, y, col) {
      mixin ColorTrait(col);
      mixin Point(x, y);
    }
  }
  const p = ColoredPoint(0,2,'red');
  p.color() // 'red'

Examples adapted from How to Node article on TraitsJS which are adapted from the original traits paper.

  trait Equality {
    requires equals(other);
    public differs(other) { return !this.equals(other); }
  }
  trait Magnitude {
    mixin Equality;
    requires smaller(other);
    public greater(other) { return !this.smaller(other) && this.differs(other); }
    public between(min, max) {
      return min.smaller(this) && this.smaller(max);
    }
  }
  class BareCircle {
    new(center, radius) {
      public center = center;
      public radius = radius;
    }
    public area() { return Math.PI * this.radius * this.radius; }
    public equals(other) { return this.center.equals(other.center) &&
                                  this.radius === other.radius; }
    public smaller(other) { return this.radius < other.radius; }
  }
  class Circle {
    mixin Magnitude;
    mixin BareCircle;
    new(center, radius) {
      mixin BareCircle(center, radius);
    }
  }
  trait ColorTrait {
    new(rgb) {
      public get rgb() { return rgb; }
    }
    public equals(col) { return col.rgb.equals(this.rgb); }
  }
  class Circle {
    mixin Magnitude;
    mixin ColorTrait with equals as equalColors; // conflict avoidance by rename
    mixin BareCircle;
    new(center, radius) {
      mixin ColorTrait(rgb);                     // mixin chaining
      mixin BareCircle(center, radius);
    }
  }
// or
  class Circle {
    mixin Magnitude;
    mixin Color without equals;                  // conflict avoidance by exclusion
    mixin BareCircle;
    new(center, radius) {
      mixin ColorTrait(rgb);
      mixin BareCircle(center, radius);
    }
  }
// or
  class Circle : ColorTrait {                    // conflict avoidance by overriding
    mixin Magnitude;
    mixin BareCircle;
    new(center, radius) {
      super(rgb);
      mixin BareCircle(center, radius);
    }
  }
 

Example adapted from TraitsJS Tutorial.

  trait EnumerableTrait {
    requires forEach(fun);
 
    public map(fun) {
      const seq = [];
      this.forEach(function(e,i) {
        seq.push(fun(e,i));
      });
      return seq;
    }
 
    public filter(pred) {
      const seq = [];
      this.forEach(function(e,i) {
        if (pred(e,i)) {
          seq.push(e);
        }
      });
      return seq;
    }
 
    public reduce(init, fun) {
      let result = init;
      this.forEach(function(e,i) {
        result = fun(result, e, i);
      });
      return result;
    }
  }
 
  class BareInterval {
    new(min, max) {
      public start = min;
      public end = max;
      public size = max - min;
    }
    public toString() { return ''+min+'..!'+max; }
    public contains(e) { return min <= e && e < max; }
    public forEach(consumer) {
      for (let i = min; i < max; i++) {
        consumer(i,i-min);
      }
    }
  }
  class Interval {
    mixin EnumerableTrait;
    mixin BareInterval;
    new(min, max) {
      mixin BareInterval(min, max);
    }
  }
// or
  class Interval : BareInterval {
    mixin EnumerableTrait;
    new(min, max) {
      super(min, max);
    }
  }
 
 
 
  const i = Interval(0,5);
  i.start // 0
  i.end // 5
  i.reduce(0, function(a,b) { return a+b; }); // 0+1+2+3+4 = 10

Adapted from explanations of Traceur

Examples adapted from Traceur Classes and Traits.

  class Monster : Character {
    new(x, y, name) {
      super(x, y);
      public name = name;
      private let health_ = x+y;
    }
 
    static public let totalAttacks = 0;
 
    public attack(character) {
      totalAttacks++;
      super.attack(character);
    }
 
    public isAlive() { return this.health > 0;
    public get health() { return private(this).health_; }
    public set(newHealth) {
      if (newHealth < 0) { throw new Error('Health must be non-negative.');
      private(this).health_ = newHealth;
    }
  }
  trait ComparableTrait {
    requires lessThan(other);
    requires equals(other);
 
    public lessThanOrEquals(other) {
      return this.lessThan(other) || this.equals(other);
    }
    public greaterThan(other) {
      return other.lessThan(this);
    }
    public greaterThanOrEquals(other) {
      return other.lessThan(this) || this.equals(other);
    }
    public notEquals(other) {
      return !(this.equals(other));
    }
  }
 
  trait BareInterval {
    new(min, max) {
      public start = min;
      public end = max;
      public size = max - min + 1;
    }
    public lessThan(ival) {
      return this.end <= ival.start;
    }
    public equals(ival) {
      return this.start === ival.start && this.end === ival.end;
    }
  }
 
  class Interval {
    mixin ComparableTrait;
    mixin BareInterval;
    new(min, max) {
      mixin BareInterval(min, max);
    }
  }
 
  var i1 = new Interval(0, 5);
  var i2 = new Interval(7, 12);
  alert(i1 + ' == ' + i2 + ': ' + i1.notEquals(i2)); // true
  alert(i1 + ' < ' + i2 + ': ' + i1.greaterThan(i2)); // false

Open Issues

See

 
strawman/classes_with_trait_composition.1305060283.txt · Last modified: 2011/05/15 02:59 by markm
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki