I've recently been experimenting with coding both prototypal and class based 
object definitions using the various syntactic forms that are currently on the 
table.  Something has emerged from that which has surprised me.  I have never 
been a big fan of the "extend" method that is provided by a number of 
JavaScript frameworks. However, based upon my experiments I'm now think that 
something like extend solves several issues with the declarative object and 
class declarations that we have recently been discussing.

Just in case there is anyone on this list who isn't familiar with extend, here 
is a quick explanation. In most frameworks that support it, "extend" is invoked 
something like this: 
   obj.extend(extensionObj)
It copies the own properties of  extensionObj and makes corresponding own 
properties for obj.  Exact details about which properties are copied, various 
error conditions and even the name of the method varies among frameworks.  It 
is generally has been used as an imperative supplement to ECMASript's built-in 
prototype inheritance, for example to provide effects similar to multiple 
inheritance. 

The use cases that interests me are some what different then this current 
common use. 

But first, I'll cut to the chase. Here is a quick summary of what I'm going to 
propose:

In addition to  <| we need another operator <& that is similar to the "extend" 
method in various frameworks.  It replicates the own properties of its RHS 
operation on its LHS operand.  It provides an easy declarative way to describe 
the properties that need to be added to an already created object. For example:

obj <& {
   __constDataProp: x,
   method(a) {return a+this._constDataProp},
   get konst() {return this.__costDataProp}
};

adds three properties to obj. 
   
So, on to the use cases that motivates this. In 
https://mail.mozilla.org/pipermail/es-discuss/2011-July/015792.html  I used an 
prototypal inheritance pattern in an example.  A slightly simplified version of 
the example is:

const Point = {
     //private members
     __x: 0,
     __y: 0, 
     __validate(x,y) { return typeof x == 'number' && typeof y = 'number'},
     //public members
     new(x,y) {
          if (!this.__validate(x,y)) throw "invalid";
          return this <| {
                  __x: x,
                  __y: y
                 }
      };
}

In this pattern, the "new" method on a prototype object is used to create 
instance of the corresponding object abstraction.  It does this by using the <| 
operator to create the instance as an object whose [[Protoype]] is set to the 
prototypal instance.  The object literal on the right of the <| lists the per 
instance state of the new object.  In this case the properties named "__x" and 
"__y".   This is a nice declarative way to describe the per instance state but 
it turns out it doesn't generalize very well to multiple levels of inheritance. 
 If you tried to use this pattern to create a three dimensional point,  it 
would probably look something like:

const Point3D = Point <| {
     //private members
     __z: 0,
     //public members
     new(x,y,z) {
          if (!this.__validate(x,y) || typeof z != 'number') throw "invalid";
          return this <| {
                  __x: x,
                  __y: y,
                 __z: z
                 }
      };
}

Note that the "new" method in Point3D had to essentially copy everything that 
was in the "new" method of Point.  This isn't very good use of inheritance. It 
also may not work if true private properties are used instead of just a naming 
convention. What you would really like to do is to let the implementation of 
"new" in Point do all the work that relates to x and y and only have to include 
in Point3D code that relates to z. You might be tempted to write it as:

const Point3D = Point <| {
     //private members
     __z: 0,
     //public members
     new(x,y,z) {
          if (typeof z != 'number') throw "invalid";
          return super.new(x,y) <| {
                 __z: z
                 }
      };
}

However, that wouldn't create what was probably intended. Instead of creating a 
single instance object that inherits from Point3D it creates two objects, the 
first one has Point3D as its [[Prototype]] and has own properties "__x" and 
"__y".  The second object has the first object as its [[Prototype]] and as 
"__z" as an own property.  If a Point4D was created following the same pattern 
the per instance state of create by each call of Point4D would be spread over 
three objects.  The problem is that we want to do a super.new call at each 
level of the inheritance hierarchy to ensure that we do all the necessary 
initialization without unnecessary code copying.  However, each level is doing 
a <| which creates a distinct object.  Instead, what we really want to do is 
create a single object at the top of the inheritance hierarchy and then add 
additional properties to that object at each inheritance level. If we want to 
do that declaratively, we need something like the extend operator. For example:


const NewablePrototype = {new() {return { }};  //define a base prototype with a 
"new" method that creates a new object

const Point = NewablePrototype <| {
     //private members
     __x: 0,
     __y: 0, 
     __validate(x,y) { return typeof x == 'number' && typeof y = 'number'},
     //public members
     new(x,y) {
          if (!this.__validate(x,y)) throw "invalid";
          return super.new() <& {
                  __x: x,
                  __y: y
                 };
      };
}

const Point3D = Point <| {
     //private members
     __z: 0,
     //public members
     new(x,y,z) {
          if (typeof z != 'number') throw "invalid";
          return super.new(x,y) <& {
                 __z: z
                 };
      };
}

Basically, <| is exactly what we want for declaratively specify inheritance 
relationships but for actually constructing instances we want <&.

This same issues also shows up with constructor based abstraction.  The 
constructor equivalent of Point and Point3D might look like this without extend:

function Point(x,y) {
   this.__x = x;
   this.__y = y;
};
Point.prototype.__validate(x,y) { return typeof x == 'number' && typeof y = 
'number'};

function Point3D(x,y,z) {
   if (typeof z != 'number') throw "invalid";
   Point.call(this,x,y);
   this.__z = z;
};
Point3D.prototype = Point.prototype <| {  //need to wire up prototype 
inheritance
   construtor: Point3D}    //and link prototype back to constructor
};

Note that this requires imperative property creation at several places.  These 
can be expressed in a more declarative style using <&:

function Point(x,y) {
   return tthis <& {
                  __x: x,
                  __y: y
                 };
};
Point.prototype <& {
   __validate(x,y) { return typeof x == 'number' && typeof y = 'number'}
};

function Point3D(x,y,z) {
   if (typeof z != 'number') throw "invalid";
   return Point.call(this,x,y) <& {__z : z};
};
Point3D.prototype = Point.prototype <| { //need to wire up prototype inheritance
   construtor: Point3d}   //and link prototype back to constructor
};

A similar pattern also shows up using the currently proposed Harmony class 
declarations:

class Point  {
     private __validate(x,y) { return typeof x == 'number' && typeof y = 
'number'};
     constructor(x,y) {
          if (!this.__validate(x,y)) throw "invalid";
          private __x: x;
          private __y: y;
      };
}

class Point3D extends Point  {
     constructor(x,y,z) {
          if (typeof z != 'number') throw "invalid";
          super(x,y);
          private __z: z;
      };
}

There are a couple things to note about the above.  First, a new member 
declaration statement-like forms (private and public) needed to be added so 
they could be used in the constructor body to define per instance properties. 
Also there is now an implicit requirement that a constructor return the same 
new object and never a substitute object. That is because the private/public 
property declarations implicitly reference the original this value passed to 
the constructor.  The need for the new private/public constructor declarations 
and the implicit this linkage could both be eliminated if the extend operator 
was used to set per instance properties within constructors:


class Point  {
     private __validate(x,y) { return typeof x == 'number' && typeof y = 
'number'};
     constructor(x,y) {
          if (!this.__validate(x,y)) throw "invalid";
          return tthis <& {
                  private __x: x,
                  private __y: y
                 };
      };
}

class Point3D extends Point  {
     constructor(x,y,z) {
          if (typeof z != 'number') throw "invalid";
          return super(x,y) <& {private __z: z};
      };
}

Note that in the above example, I assume that object literals can support 
"private" property declarations in a similar manner to class declaration.

Another use case for extend is adding properties to function objects.  
Currently if you want to add properties to a function you have to do it 
imperatively like:

function Point(x,y) {
    this.__x = x;
    this.__y = y;
};

Object.defineProperty(Point, "origin", {get: function() {return new 
this(0,0)}});  //note this will be Point constructor

The <| allows us to declaratively add inherited properties to a function but no 
way to make them own properties: 

let Point = Function.prototype <| {get origin() { new this(0,0)}}  <| function 
(x,y) {
    this.__x = x;
    this.__y = y;
};

The extend operator would change that:

let Point = function (x,y) {
    this.__x = x;
    this.__y = y;
    }  <&  {
    get origin() { new this(0,0)}
};

Note that some might object to the need to use a let (or const) declaration 
rather than a function declaration in the above example.  That could be easily 
resolved by syntactically extending FunctionDeclaration (and similar syntactic 
forms) to allow the function body to be followed by <& and an object literal:

function Point (x,y) {
    this.__x = x;
    this.__y = y;
    }  <&  {
    get origin() { new this(0,0)}
};

A similar issue exists for the proposed class declarations.  That proposal 
includes the concept of "static" (a lot of us don't like that the term "static" 
in this context)  property declaration:

class Point  {
     private __validate(x,y) { return typeof x == 'number' && typeof y = 
'number'};
     constructor(x,y) {
          if (!this.__validate(x,y)) throw "invalid";
          return tthis <& {
                  private __x: x,
                  private __y: y
                 };
      };
     static get origin() { new this(0,0)};
}

 Static properties are really just own properties of the constructor object.  
While sometimes useful, they occur relatively infrequently yet they require a 
additional declaration form within class bodies.  This complicates the 
conceptual model of a class declaration by allowing intermingling of 
constructor and prototype property declaration.  This also increase the 
potential for confusion about the meaning of "this"  (and "super") within such 
static property declarations.  The need for the static declaration could be 
eliminated by using the extend operator instead:

class Point  {
     private __validate(x,y) { return typeof x == 'number' && typeof y = 
'number'};
     constructor(x,y) {
          if (!this.__validate(x,y)) throw "invalid";
          return tthis <& {
                  private __x: x,
                  private __y: y
                 };
         }  <&  {
            get origin() { new this(0,0)}
        }
};

or since the value of a class declaration is its constructor:

class Point  {
     private __validate(x,y) { return typeof x == 'number' && typeof y = 
'number'};
     constructor(x,y) {
          if (!this.__validate(x,y)) throw "invalid";
          return tthis <& {
                  private __x: x,
                  private __y: y
                 };
      } 
}  <&  {
      get origin() { new this(0,0)}
 };

Either or both of these alternatives could be made syntactically legal and note 
that the method declaration form could be applied to any method declaration in 
an object literal or class declaration, not just the constructor. Either 
alternative eliminates the need for a separate static declaration form and 
segregates constructor properties which should avoid confusion between them and 
prototype properties.


So, why use an operator like <& instead of a method named "extend":
1)  A big reason is that frameworks already use the extend name and the exact 
semantics we would define for it are only to match the current semantics of 
these frameworks.  By not using that name we avoid compatibility issues.
2) A operator form such as <& avoids issue of method redefinition and can more 
easily added to existing syntactic forms such as function declarations.
3) It is conceptually natural a natural companion to <|.  It makes sense to 
learn about the two operators together (particularly with regard to object 
literals on the RHS).


I think <& is a natural companion to <| and is in line with our goals to both 
enhancing object literals and providing a class declarations as alternative to 
stand-alone constructor functions.  It increases the expressiveness of object 
literals for declaratively defining prototypal object models and permits 
simplifications of the proposed class declaration form.

Allen







_______________________________________________
es-discuss mailing list
[email protected]
https://mail.mozilla.org/listinfo/es-discuss

Reply via email to