An experiment using an object literal based class definition pattern

Allen Wirfs-Brock allen at wirfs-brock.com
Thu Aug 4 13:57:48 PDT 2011


This is partially a followup to the thread:  An "extend" operator is a natural companion to <|

Several interesting ideas developed out of informal conversations during last week's TC-39 meeting.

The first ideas concerns the  <| operator.  As currently defined at http://wiki.ecmascript.org/doku.php?id=harmony:proto_operator a usage such as:
    function foo () {};
    foo.prototoype.method = function() {};
    const bar = foo <| function() {};

assigns to bar a function whose [[Prototype]] internal property is the value of foo.  However, as current specified the [[Prototype]] of bar.prototype will be Object.prototype.  In other words,  bar inherits from foo but bar.prototype doesn't inherit from foo.prototype.  That seems unlikely to be the desired behavior in most situations.  We can fix that by specifying that if the RHS of <| is a function expression and the LHS is an object with a "prototype" property then the object that is the value of the "prototype" property of the new function object inherits from LSH.prototype rather than Object.prototype.  I'll assume that definition for the rest of this posting.  It means that for the above statements, the following identities hold:
   Object.getPrototypeOf(bar)===foo;  //true
   Object.getPrototypeOf(bar.prototype)===foo.prototype;  //true

Also note that the LHS does not need to be a function for this to work.  You might have defined:
  const foo = Function.prototype <| {
     aConstructorProperty: whatever,
     prototype: {
       anInstanceProperty: whatever
     }
  };

and the above identities would still hold. 
   
Another idea was an alternative way to express the "extend" operator for literal property definitions.  Doug Crockford suggested the following syntax:

    obj.{
       prop:1,
       prop:2
       }

This takes the properties defined in the object literal and adds then to obj (or replaces like named already existing properties).  An exception is thrown if they can't all be added.  Essentially .{ } is a postfix operator that extends an object with additional properties.

Using these two new ideas and other active object literal enhancement proposals it is pretty easy to compose a class-like declaration.  For example the SkinnedMesh from the http://wiki.ecmascript.org/doku.php?id=harmony:classes proposal can be code as:

  const SkinnedMesh = THREE.Matrix4.Mesh <| function(geometry,materials){
    super.construtor(geometry,materials);
    this.{
      identity.Matrix: new THREE.Matrix4(),
      bones: [],
      boneMatrices: []
    };
  }.prototype.{
    update(camera) {
      ...
      super(update);
    }
  }.constructor.{
    default(){
      return new this(THREE.defaultGeometry,THREE.defaultMaterials);
    }
  };
        

Note that I added a constructor (ie, "static") method as one wasn't included in the original example.  

This definition is very similar to the one in the classes proposal but in completely defined using object literals.  Here is the generalized code pattern for such "class" definitions:

const className = superClass <| function(/*constructor parameters*/) {
  /*constructor body*/
  super.constructor(/*arguments to super constructor*/);
  this.{
    /* per instance property declarations */
  };
  /* other constructor code */
}.prototype.{
  /*instance properties defined on prototype*/
).constructor.{
  /*class (ie, constructor "static") properties */
};
 
Another idea that was discussed and which I think we are near to reaching consensus on is how to define a private named property in an object literal (or class declaration). As has been suggested in the past, it would be done by using a bracketed expression in the property name position.  For example:

  const pname1 = Name.create();  //create a new private name and bind it to a const
  const pname2 = Name.create();  
  let obj = {
    regularName: 0,
    [pname1]: 1,  // a data property with a private name
    [pname2]() {reurn this[pname1]} //a method data property with a private name
  };

I wanted to evaluate these feature proposals and coding patterns with something more substantial then a 5 line synthetic example.  What would it be like to actually write real class based code using them.  So, as an experiment, I decided to see what the Smalltalk-80 collection hierarchy would look like coded in ES.next using these ideas and patterns.   The Smalltalk-80 collections hierarchy is a good test case because it is a well-known class hierarchy that exercises pretty much the full gambit of features that are useful in building complex single inheritance hierarchies. It is relatively deep (6-levels in my experiment), uses both class and instance side inheritance, make extensive use of super method calls, "protected" methods, and inherited private instance state.  Its basic design has been in use for over thirty years and is one of the original object-oriented hierarchies that pretty much established the pattern of how to used OO implementation inheritance. You may quibble about some of the design approaches and implementation techniques used in this vintage hierarchy, but I think it is reasonable to expect that any fully featured dynamic OO language should be expressive enough to support an implementation of this class hierarchy.

My implementation is based upon the description in chapter 13 of the Smalltalk-80 "Blue Book" (http://stephane.ducasse.free.fr/FreeBooks/BlueBook/ ) along with an occasional peak at the open source Squeak Smalltalk-80 implementation (http://ftp.squeak.org/1.3/SqueakV1.sources.gz ).  It isn't a complete implementation but is complete enough to get a pretty good feel for what it is like to write and read code written in this style. Oh, and did I mention, that is is completely untested and probably wouldn't even syntax check correctly if I had a parser for this ES.next alternative.

The full source file of this experiment is at  https://github.com/allenwb/ESnext-experiments/blob/master/ST80collections-exp1.js . I encourage you to look closely at the whole thing, think about these coding pattens for defining "classes", and to make your feedback here. 

As teaser, here is the code for one of the classes:

/*-------------------------- ArrayCollection --------------------------*/
/* I am an abstract collection of elements with a fixed range of integers
(from 1 to n>=1) as external keys.
*/
export const ArrayedCollection = SequenceableCollection <| function(elements=0) {
  super.constructor(elements);
}.prototype.{
  //accessing protocol
  get size() {return this.basicSize},
  at(index) {return this.basicAt(Math.floor(index))},
  atPut(index,value) {return this.basicAtPut(Math.floor(index),value)},
  //adding protocol
  add(newObject) {this.shouldNotImplement()},
  //protected methods for storage access
}.constructor.{
  newWithAll(size,value) {
    return (new this(size)).atAllPut(value);
  },
  with(...args) {
    const newCollection = new this(args.length);
    let i = 1;
    args.forEach(function(element) {newCollection.atPut(i++,element)});
    return newCollection;
  },
  className: "ArrayedCollection",
 };


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20110804/d7fc29de/attachment.html>


More information about the es-discuss mailing list