Allowing super to use EvaluateConstruct, or "How do I inherit from Date?"

# Brandon Benvie (13 years ago)

Despite separating out @@create into a property of functions, which allows allocation and setting of BuiltinBrand for subclasses, this still leaves inheriting from most builtins just short of possible because they are "construct sensitive". The new ES6 classes like Map, Set, WeakMap, and typed arrays have been carefully designed so the constructor initializes this. But this still leaves most of the existing builtins un-subclassable. I know there's a strawman that addresses this, and many of the recent changes to the ES6 spec have been moving toward making this possible, but I don't recall seeing commentary on a planned solution for this.

It seems like the last hurdle to actually making this work is simply making it possible for super to just "forward" to EvaluateCall or EvaluateConstruct depending on which of them invoked the function that the super call was in. Is this solution possible for ES6? Or maybe there's another plan in the works to address this? Or is this something that can be looked for in harmony (which I presume now refers to es-after 6/es7)?

# Erik Arvidsson (13 years ago)

On Sun, Jan 6, 2013 at 10:32 PM, Brandon Benvie <brandon at brandonbenvie.com> wrote:

Despite separating out @@create into a property of functions, which allows allocation and setting of BuiltinBrand for subclasses, this still leaves inheriting from most builtins just short of possible because they are "construct sensitive". The new ES6 classes like Map, Set, WeakMap, and typed arrays have been carefully designed so the constructor initializes this. But this still leaves most of the existing builtins un-subclassable.

I think you are making this sound worse than it is.

What isn't working is that super in the constructor doesn't do what you expect. The following should work

class MyArray extends Array { constructor(...args) { // Intentionally not doing the crazy one-arg-is-length-if-uint32 this.push(...args); } }

class MyDate extends Date { constructor(..args) { var temp = new Date(...args); this.setTime(temp.getTime()); } }

I agree that it will be confusing to most users and that we should try to make super in a constructor work for these cases.

-- erik

# Brandon Benvie (13 years ago)

Yeah, that's a better way to say what I (poorly) expressed. It's not that EvaluateConstruct needs to again be called (this would cause multiple allocations). Rather, the super call that arose directly from EvaluateConstruct needs to pass its "constructness" onto the SuperConstructor.

# Brandon Benvie (13 years ago)

Also it's not just confusing. Arrays and Date can be jury rigged to function correctly with the recent ES6 spec changes, as you demonstrated. That still leaves it impossible to extend Boolean, Number, String, and RegExp (leaving out Function on purpose since that's in a league of its own and would depend on @@call being added) because they are construct sensitive and have no post-creation way of mutating their internal value.

# Allen Wirfs-Brock (13 years ago)

On Jan 7, 2013, at 8:46 AM, Brandon Benvie wrote:

Also it's not just confusing. Arrays and Date can be jury rigged to function correctly with the recent ES6 spec changes, as you demonstrated. That still leaves it impossible to extend Boolean, Number, String, and RegExp (leaving out Function on purpose since that's in a league of its own and would depend on @@call being added) because they are construct sensitive and have no post-creation way of mutating their internal value.

They all can be jury rigged. At the level of the specification I can factor the initialization between the @@create function and the built-in constructor pretty much any way I want while still preserving external immutability of the internal data properties. ES code can doing similar things via private symbols. The patterns I use for this jury rigging work for both direct and super calls to the constructors.

I think the real issue is whether we want to encourage the style of constructor where "called as a constructor" and "called as a function" do completely different things. I'm leaning towards the conclusions that we shouldn't. One reason is that the language is fighting so hard against that pattern. Anything that requires multiple low level hacks to the language semantics (and possibly new syntax) to make it "just work" probably is something that shouldn't be done.

For the non-spec readers out there, here is the jury rigging pattern. Every "class" in the spec. that has different constructor/function call behavior has (will have) a @@create method that allocates its instances and in the process tags the instance with a brand and perhaps a not yet initialized flag. In a new expression, the @@create call happens before the call to the actual constructor function and the result of the @@create call is what is passed as the this value to the constructor.

The constructor function, itself, is defined to determine its behavior based upon the this value that is passed to it. It the this value is branded with the instance brand, the constructor initializes and returns the instance according to its normal definition. Otherwise, if the this value is not an object or an object without the instance brand then the constructor performs the "call" functionality.

For the existing built-in this jury rig appears to be adequate for ensuring compatibility with all existing code. Consider all the ways that, for example, Number can currently be "called as a function":

let n; n = Number("1234"); //this is undefined so jury rig takes ToNumber path within constructor. n = Number.call(null, "1") //this is null so jury rig takes ToNumber path within constructor. n = Number.call(1, "1") //this is not an object so jury rig takes ToNumber path within constructor. n = Number.call(new Object, "1"); //this is an object without the number brand so jury rig takes ToNumber path within constructor. n = {foo: Number}.foo("1"); //this is an object without the number brand so jury rig takes ToNumber path within constructor. n = Number.call(new Number("2"), "1"); //this is an object with the number brand but it is already initialized so (for backwards compat) jury rig takes ToNumber path within constructor. Number.prototype.bar = Number; n=new Number(2).bar("1"; //this is an object with the number brand but it is already initialized so jury rig takes ToNumber path within constructor.

But in ES6 (now backwards compat issues here): class MyNumber extends Number { additionalNumberMethod () {} }

n = new MyNumber("1"); //default constructor does super.constructor call with new instance created by Number.@@create, so initialization path taken with Number.

n = MyNumber("1") //default constructor does super.constructor call with undefined as this value, so ToNumber path taken and Number(1) is returned. But if MyNumber wants to also expose its own explicit conversions behavior it will need to provide its own constructor that decides when to take that path

Here is a ES level example, of defining a class that uses the "called same as new" pattern:

const $myBrand = new Symbol(true); import $create ...;

class MyObj { constructor () { if (typeof this !== 'object' || this == null || !Reflect.has($myBrand)) return new MyObj(); /* could check here for this already initialized / / do any initialization */ this[$myBrand] = true; //tag as initialized } } Object.mixin(MyObj, { [$create] () { let obj = super(); //call default @@create Object.defineOwnProperty(obj,$myBrand,{value: undefined, writable: true}); return obj; } });

# Brandon Benvie (13 years ago)

I agree with your conclusion and believe we could call the differentiation that has existed in the spec of "called as a constructor" an anti-pattern, in as far as it's one of those "magical" pieces of state that isn't exposed to user-level code and thus requires even more magic (say in the form of an $$CalledAsConstructor primitive) in order to detect it and self-host the stdlib.

The solution I proposed would serve the goal of making all the builtin classes subclassable, but would also perpetuate the "CalledAsConstructor" anti-pattern down to the subclass level. Your solution is better, and I'm guessing this is what you plan to do with the ES6 spec in the near future, which makes me happy!

# Herby Vojčík (13 years ago)

Allen Wirfs-Brock wrote:

I think the real issue is whether we want to encourage the style of constructor where "called as a constructor" and "called as a function" do completely different things. I'm leaning towards the conclusions that we shouldn't. One reason is that the language is fighting so hard against that pattern. Anything that requires multiple low level hacks to the language semantics (and possibly new syntax) to make it "just work" probably is something that shouldn't be done.

Well, I would disagree strongly.

The "problem" here stems from tightky coupling the constructor function to the "class". Those two things can be very cleanly and minimally sepaerated.

Read my "Good bye constructor functions?" post from Dec 30. This pattern (class = constructor) is very deeply grown into thinking of JS developer, but it is not the law. It is very easy to get rid of it.

Then, nothing from below is ever needed. It all follows naturally. Anf without hacks.