An "extend" operator is a natural companion to <|

Allen Wirfs-Brock allen at wirfs-brock.com
Mon Jul 18 11:44:49 PDT 2011


On Jul 18, 2011, at 11:29 AM, Sean Eagan wrote:

> It would also allow declarative non-integer own properties for arrays,
> and arbitrary own properties to regular expressions, numbers,
> booleans, and strings, though I can't think of any specific use cases
> for those off of the top of my head.

Yes, I should have mentioned at least the array case.

> 
> Also, how about |> as opposed to <&, since it is a dual to <| adding
> own rather than inherited properties?

I'd be a bit concerned about  some people getting confused about which direction of the "arrow" corresponds to each operation.  Might be particularly hard for people with dyslexia.  I have played around with some other possibilities, for example, +<| .

I like the nemonic value of having + or & as part of the operator symbol for "extend" but  unfortunately <+ can't be used.

Allen






> 
> On Mon, Jul 18, 2011 at 12:32 PM, Allen Wirfs-Brock
> <allen at wirfs-brock.com> wrote:
>> 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
>> es-discuss at mozilla.org
>> https://mail.mozilla.org/listinfo/es-discuss
>> 
>> 
> 
> 
> 
> -- 
> Sean Eagan



More information about the es-discuss mailing list