This is an old revision of the document!
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.
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.
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:
“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.“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.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:
“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.“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.
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.
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
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.
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
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
object_initialiser_extensions, especially obj_initialiser_class_abstraction and instance_variables
Encapsulation and Inheritance in Object-Oriented Programming Languages – classic 1986 paper by Alan Snyder
inherited explicit soft fields
How To Node article on Traits.js
Classes as Sugar thread which starts with pointers to earlier threads.
classes as inheritance sugar (not yet ready)