Must built-in prototypes also be valid instances? (Was: Why DataView.prototype object's [[Class]] is "Object"?)
allen at wirfs-brock.com
Sat Sep 29 21:47:58 PDT 2012
On Sep 29, 2012, at 5:17 PM, Brendan Eich wrote:
> Allen Wirfs-Brock wrote:
>> However, there is a bigger issue here. You could have asked a similar question about Map.prototype. Why isn't Map.prototype a Map instances? Is this a bug or an intentional design decision?
>> Historically, the prototype objects associated with the 9 named built-in constructors were all defined to be instances of those constructors. For example, Boolean.prototype is a Boolean instance whose value is false.
>> In writing the specification for Map, I intentionally deviated from that pattern.
> Failing to consult with implementors will just make editing churn. I don't remember much discussion on this change, if any -- we did talk about the general problem of prototype objects being firstborns of their class, and how this makes them sometimes not just basis cases but degenerate cases.
That's why I brought it up, so we can talk about it.
But it's not a change, Map is new. The strawman didn't address this issue unless you place significance in the fact that the spec. uses a class definition to specify the behavior of Map. If you do consider that significant, then what I spec'ed is consistent with that because, as I pointed out in my message, ES6 class definitions don't maintain the prototype is an instance of the class invariant.
> However, Map is prototyped in SpiderMonkey and V8. In SpiderMonkey, the prototype is a Map, not an Object:
How do you define "is a Map"?
> js> Object.prototype.toString.call(Map.prototype).slice(8, -1)
The draft spec. for Map will also produce this result. But is returning "Map" from toString the condition make something "a Map"? (See the draft spec. for Object.prototype.toString)
> but you can't get, has, set, size, or iterate Map.prototype:
> js> Map.prototype.get('x')
> typein:2:0 TypeError: get method called on incompatible Map
> js> Map.prototype.has('x')
> typein:3:0 TypeError: has method called on incompatible Map
> js> Map.prototype.set('x', 42)
> typein:4:0 TypeError: set method called on incompatible Map
> js> Map.prototype.size()
> typein:5:0 TypeError: size method called on incompatible Map
> js> for (var [k, v] of Map.prototype) ;
> typein:6:13 TypeError: iterator method called on incompatible Map
The draft spec. produces exactly these results (of course it doesn't provide the error message text"). However, for the above test cases of the SpiderMonkey, if Map.prototype is really a Map instance then why don't these methods do something useful, rather than throwing a TypeError? get could return undefined, has could return false, size could be 0, @iterator, could return an "empty" iterator, etc. To me, that is what "is a Map" means. All the behaviors for Map instances work as specified.
> The error message is suboptimal but what's going on here is that Map.prototype has the SpiderMonkey equivalent of [[Class]] == "Map" (or the ES6 equivalent).
The equivalent in the spec. draft is to test whether or not the object is an implementation of the specified Map instance abstraction. Within the draft spec. this is abstracted as testing whether or not the object has a "[[MapData]]" internal property. How implementations choose to test that conditions and represent the associated state is their decision to make.
> This is important since all the builtins in ES3 + Reality (including RegExp; ES3 deviated there) make the prototype for built-in class C be of class C.
Yes, but why is that important to anyone? In my earlier message on thus thread I argued that it is in fact unimportant and probably undesirable. Other than legacy consistency what makes this an important invariant to maintain?
> Your change requires implementations to provide a different built-in class (constructor/prototype) initialization path. That's not desirable _per se_. Neither IMHO is the user-facing semantic split among "old" and "new" constructors.
There are costs and benefits of changes. In this particular case, the implementation cost of supporting prototypes that are not also instances of their associated constructor seems likely to be very low. As you mention, prior to ES5 RegExp was specified that way and it appears (at least for some implementations) that (some) DOM objects don't have the characteristic.
> There are two separate issues here:
> 1. Should Map.prototype be an instance (firstborn for its realm) of class Map?
> 2. Should Map.prototype be a key/value store that can be used or abused as any other Map could be?
> We should not mix these up. SpiderMonkey (and possibly V8, I haven't tested) says "yes" to 1 and "no" to 2.
I have no idea what you actually mean by "an instance" or "firstborn of its realm".
In ES<6 specifications. 1 and 2 above are the same thing. Being "an instance of a built-in constructor" in those specifications means that an object has all of the internal and regular properties as specified for instances created by the constructor (except that its [[Prototype]] value is different). The prototype can be used as a regular instance of the constructor.
>> I did not specify that Map.prototype is a Map instance. While it has the methods that are applicable to Map instances it does not the internal state that is necessary for those methods to actually work. For example, if you try to store a value into Map.prototype by calling its set method, you will get a TypeErrior according to the specification. May.prototype can not be used as a map object.
> That doesn't mean Map.prototype should not be classified as a Map per 1 above.
If it behaviorally doesn't work according to the Map specification, why should it be considered a Map.
>> Why did I do this? Because, we are defining a significant number new built-in "classes" and it is going to be increasingly hard to define meaningful instance state for all such prototypes.
> Not so, as shown above. It's trivial in SpiderMonkey to give the prototype no instance state and check for that. Alternative implementation techniques are feasible with different trade-offs.
And then, by the specification conventions of ES<6, it is not an instance of the constructor. That's all I've done here, confronted the lie.
However, lurking is all sorts of interesting questions that we've previous agreed that we need to discuss more here: nominal typing of builtins, the future of [[Class]], the future of Object.prototype.toString, subclassing of builtins, etc.
It's probably time to start that discussion again.
More information about the es-discuss