Subclassing an array and array methods
My intent is to use a private name property to allow an object to provide a constructor for new "arrays" derived" from it. something along the lines of:
function createArraySubclass(proto,...values) { return proto <| [...values].{ Array.derivedArrayKey{return proto<| [ ]} } }
The built-ins would create the derived collection by doing the equivalent of:
// assume |this| is the source collection
if (this[Array.derivedArrayKey]) A = this[Array.derivedArrayKey](); else A = [ ];
I can't just take the [[Prototype]] of the source array because that would introduce an incompatibility with existing code that uses these functions on non-Array objects. Also if we are generalizing these functions we shouldn't assume that all collections will what their derived collections to be "subclasses" of Array. They might want to allocate something else entirely.
function createArraySubclass(proto,...values) { return proto <| [...values].{ Array.derivedArrayKey{return proto<| [ ]} } }
I’m curious: Why wouldn’t one extend Array, instead?
function SubArray() { } SubArray.prototype = Object.create(Array.prototype); SubArray.prototype.pushAll = function() { Array.prototype.push.apply(this, arguments); return this; } var s = new SubArray().pushAll(3,4,5);
On Nov 11, 2011, at 9:47 AM, Axel Rauschmayer wrote:
function createArraySubclass(proto,...values) { return proto <| [...values].{ Array.derivedArrayKey{return proto<| [ ]} } }
I’m curious: Why wouldn’t one extend Array, instead?
the problem is with built-ins like Array.prototype.filter that produce a new collection object. Today the new collection is always an Array, but for some operations you would really like the new collection to be the same kind of collection as original collection or something other than Array.
In Smalltalk (and I believe self) this is done by collections having a methods named "species" that returns the class that should be used to create the new collection. Part of the design of a new collection class is the assignment of its "species". In my sketch above, a call to the private derivedArray method is the equivalent to saying: self species new in Smalltalk.
So, if I have an object created by creteArraySubclass and say:
let f = obj.filter(function(element) {return isGood(element)});
then f will be an instance of the array subclass rather than a direct instance of Array.
This also would work if obj was any generic array-like collection that had a derivedArray method. Even if the collection didn't inherit from Array.prototype
let f = [].filter.call(obj,function(element) {return isGood(element)});
If the built-in Array prototype functions were respecified to use elementGet/elementSet from strawman:object_model_reformation instead of [[Get]]/[[DefineOwnProperty]] to process elements then those functions would work with any collection that had integer indexed keyed data elements.
Got it, related to what you solve in generic classes with This-types (covariance vs. contravariance...).
// assume |this| is the source collection
if (this[Array.derivedArrayKey]) A = this[Array.derivedArrayKey](); else A = [ ];
Another possibility:
if (this[Array.derivedArrayKey]) {
A = this[Array.derivedArrayKey]();
} else if (typeof this.constructor === "function") {
A = new this.constructor();
} else {
A = [ ];
}
Creating a subclass this way (i.e., using @derivedArrayKey) produces code whose meaning is more obvious (to me) than the code below.
On Nov 11, 2011, at 10:52 AM, Axel Rauschmayer wrote:
Got it, related to what you solve in generic classes with This-types (covariance vs. contravariance...).
// assume |this| is the source collection if (this[Array.derivedArrayKey]) A = thisArray.derivedArrayKey; else A = [ ];
Another possibility:
if (this[Array.derivedArrayKey]) { A = thisArray.derivedArrayKey; } else if (typeof this.constructor === "function") { A = new this.constructor();
In Smalltalk the default implementation of species is: species ^self class
which is pretty much the moral equivalent of: this.constructor.
However, that might introduce backwards compatibility issues for existing code which has constructors but currently always produces Array instances from these functions.
} else { A = [ ]; }
Creating a subclass this way (i.e., using @derivedArrayKey) produces code whose meaning is more obvious (to me) than the code below.
I just wanted to dash out the most directly extensions of Jake's original code that would exhibit the concept. Ultimately, it's a matter of defining a method using which ever formulation you prefer (and which gets adopted into the language)
On Nov 11, 2011, at 11:07 AM, Allen Wirfs-Brock wrote:
On Nov 11, 2011, at 10:52 AM, Axel Rauschmayer wrote:
Got it, related to what you solve in generic classes with This-types (covariance vs. contravariance...).
// assume |this| is the source collection if (this[Array.derivedArrayKey]) A = thisArray.derivedArrayKey; else A = [ ];
Another possibility:
if (this[Array.derivedArrayKey]) { A = thisArray.derivedArrayKey; } else if (typeof this.constructor === "function") { A = new this.constructor();
In Smalltalk the default implementation of species is: species ^self class
which is pretty much the moral equivalent of: this.constructor.
However, that might introduce backwards compatibility issues for existing code which has constructors but currently always produces Array instances from these functions.
Right, this was what Alex Russell and I realized and sighed about, after first hoping we could use this.constructor.
} else { A = [ ]; }
Creating a subclass this way (i.e., using @derivedArrayKey) produces code whose meaning is more obvious (to me) than the code below.
I just wanted to dash out the most directly extensions of Jake's original code that would exhibit the concept. Ultimately, it's a matter of defining a method using which ever formulation you prefer (and which gets adopted into the language)
We should probably think about a systematic naming convention (and location convention) for these built-in private name objects.
I misunderstood the problem: I thought there was something peculiar about an array instance (the first object in the prototype chain), but it’s about the Array constructor not being invocable as a function (right?).
Wouldn’t it be easier to introduce a generic method Array.prototype.init() that behaves like quirk-free Array.prototype.constructor()?
Use case 1: Create an array subtype: let SubArray = Array <| function(...args) { super.init(...args); // instead of super.constructor(...args) } Use case 2: Avoid the length pitfall: var arr = new Array().init(5); // same as [5]
-
Another, possibly stupid, idea: Could Array.prototype.constructor not point to Array, but to a function like Array.prototype.init() – that doesn’t create a new instance and doesn’t have the length quirk?
-
One more: Introduce a constant CLEAN let SubArray = Array <| function(...args) { super.constructor(CLEAN, ...args); } var arr = new Array(CLEAN, 3); // the same as [3]
-
Creating a subtype CleanArray or ExtensibleArray also seems feasible. Engines could optimize internally so that it’s basically the constructor being swapped for a quirk-free implementation.
Whatever the solution, it’s obviously applicable to Date, Error, etc., as well.
In Smalltalk the default implementation of species is: species ^self class
which is pretty much the moral equivalent of: this.constructor.
However, that might introduce backwards compatibility issues for existing code which has constructors but currently always produces Array instances from these functions.
The problem are Array subtypes that expect slice() et al. to produce instances of Array (and not of themselves) (?)
function createArraySubclass(proto,...values) { return proto <| [...values].{ Array.derivedArrayKey{return proto<| [ ]} } }
@derivedArrayKey would normally be in the prototype, right?
On Nov 12, 2011, at 9:38 AM, Axel Rauschmayer wrote:
I misunderstood the problem: I thought there was something peculiar about an array instance (the first object in the prototype chain), but it’s about the Array constructor not being invocable as a function (right?).
I'm not sure what you mean about "not invocable as a function".
ArraY(1,2,3,4)
works just fine an creates an array instance.
Wouldn’t it be easier to introduce a generic method Array.prototype.init() that behaves like quirk-free Array.prototype.constructor()?
Use case 1: Create an array subtype: let SubArray = Array <| function(...args) { super.init(...args); // instead of super.constructor(...args) }
the problem is that the "specialness" of arrays is mostly that they have their own alternative definition of [[DefineOwnProperty]]. By the time you reach the init call, the new object has already been created with the default [[DefineOwnProperty]] behavior. It isn't clear, that it is possible or reasonable to change such internal behaviors after an object is created. myProto<| [ ] avoid the problem by forcing creation (via [ ] ) of an object with the special array [[DefineOwnProperty]].
There are potentially other ways around this problem. For example, there could be a well know private name property that could be attached to a constructor that would force |new| to create instances that use the Array internal methods.
Use case 2: Avoid the length pitfall: var arr = new Array().init(5); // same as [5]
- Another, possibly stupid, idea: Could Array.prototype.constructor not point to Array, but to a function like Array.prototype.init() – that doesn’t create a new instance and doesn’t have the length quirk?
Not compatible with existing code.
One more: Introduce a constant CLEAN let SubArray = Array <| function(...args) { super.constructor(CLEAN, ...args); } var arr = new Array(CLEAN, 3); // the same as [3]
Creating a subtype CleanArray or ExtensibleArray also seems feasible. Engines could optimize internally so that it’s basically the constructor being swapped for a quirk-free implementation.
I don't think that the length quirk is the major issue anyone is concerned about.
Whatever the solution, it’s obviously applicable to Date, Error, etc., as well.
Those are actually easer to make subclassable because they don't redefine any of the internal methods. All that is needed is to convert their private state into private named properties. That's already on my todo list.
I misunderstood the problem: I thought there was something peculiar about an array instance (the first object in the prototype chain), but it’s about the Array constructor not being invocable as a function (right?).
I'm not sure what you mean about "not invocable as a function".
ArraY(1,2,3,4) works just fine an creates an array instance.
Sorry, I meant: always produces a new instance, even when invoked as a function.
Wouldn’t it be easier to introduce a generic method Array.prototype.init() that behaves like quirk-free Array.prototype.constructor()?
Use case 1: Create an array subtype: let SubArray = Array <| function(...args) { super.init(...args); // instead of super.constructor(...args) }
the problem is that the "specialness" of arrays is mostly that they have their own alternative definition of [[DefineOwnProperty]]. By the time you reach the init call, the new object has already been created with the default [[DefineOwnProperty]] behavior. It isn't clear, that it is possible or reasonable to change such internal behaviors after an object is created. myProto<| [ ] avoid the problem by forcing creation (via [ ] ) of an object with the special array [[DefineOwnProperty]].
Right, I overlooked that: You need to “intercept” elements being set/added, in order to update length
.
Then init() would not make sense at all. I didn’t know that array instances were special, I assumed the specialness was all in Array.prototype.
There are potentially other ways around this problem. For example, there could be a well know private name property that could be attached to a constructor that would force |new| to create instances that use the Array internal methods.
That would be a great solution! Or, if one could move the specialness from array instances to Array.prototype.
It seems that the reformed [] would also allow one to create an Array variant (e.g. called Sequence, List, or Vector) that is more amenable to subtyping/extension/subclassing.
Whatever the solution, it’s obviously applicable to Date, Error, etc., as well.
Those are actually easer to make subclassable because they don't redefine any of the internal methods. All that is needed is to convert their private state into private named properties. That's already on my todo list.
And you’d need a way to invoke their constructor as a function, on an existing instance.
snip
Use case 2: Avoid the length pitfall: var arr = new Array().init(5); // same as [5]
See: Array.of() strawman:array_extras
In es-next we should be able to subclass an array
function createArraySubclass(proto, ...values) { return proto <| [...values]; }
However when we call
instanceOfSubArray.filter(...)
this method returns a new Array rather then a new SubArray.It would seem frustrating to have to overwrite all array methods that return arrays and make them return subarrays.
Can we have array methods returning new instance of [[Prototype]] to make it easier to overwrite the build in?
However this should only apply to true array subclasses. In the above example I would assume the [[Class]] of the object is Array. It should not apply to [].slice.call(arrayLikeObject)
Do we have other situations where it would make sense to return an instance of whatever prototype your operation on rather then a hardcoded new X ?