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