Feedback on "Read-only Override Prohibition is [not a mistake]"
On Jan 15, 2012, at 8:10 AM, David Bruant wrote:
Hi,
I'm refering to Allen's writing at [1]:
The basic idea is that the properties prototype object are shared parts of all of inheriting child object. Modifying such a shared part by a child, introduces a local change that is visible to that child (and its children) so this requires creation of a “own” property on the child. However, read-only properties can not modified (by normal means, eg assignment) so there is no need to create a “own” copy.
I agree with what you describe as an intention, but I don't know to what extent it applies to JavaScript (I don't know Self unfortunately):
var p = {}; var o = Object.create(p); var o2 = Object.create(p); o.a = 1; // creating an own property
Object.defineProperty(p, 'a', {value:2, configurable:false, writable:false}); // now, o and o2 "share" an 'a' property. But do they really?
The dynamicity of JavaScript makes the notion of "sharing" dynamic as well, I think.
However, the above requires Object.defineProperty which did not exist prior to ES5. Since the starting point of this conversations whether or a design "mistake" was made in the ES5, I don't think it is valid to use defineProperty in trying to understand the original (prior to ES5) design intent.
Assigning to an inherited read-only property or a “own” read-only property should have the same affect (whether it is ignoring the assignment, throwing, etc.).
// following above code o.a = 3; o2.a = 4;
Due to o having an 'a' property before inheriting from a read-only one, the notion of "same effect" cannot be applied (unless breaking other invariants).
Allowing assignment to an inherited read-only property would break the invariant that that a prototype’s readonly property is an immutable value that is shared among all children of the prototype.
If there was a mistake in designing ES5, it was allowing Object.defineOwnProperty to create child properties that over-ride inherited read-only data properties.This broke an invariant that previously existed in the language but this invariant was already violated by some pre-ES5 clause 15 objects, (eg the writability of the prototype property of some children of Function.prototype). However, I think the ES5 decision was probably the right one given the legacy clause 15 usages and the overall reflective nature of defineOwnProperty).
If I sum up, the design of Object.defineProperty was necessary for backward compatibility (pre-ES5) reasons. A consequence of this design is breaking the invariant you mentionned ("a prototype’s readonly property is an immutable value that is shared among all children of the prototype").
It was already broken in ES3 for some library objects. However, the invariant still held in ES3 for all user defined objects. Object.defineProperty allows ES programmer to "break" the invariant in the same manner that ES implementors had previous been allowed to. However, ES5 maintained the exact same semantics for assignment to an inherited read-only property as had existed in ES1-3.
Considering this invariant is broken (as i showed above, it was probably doomed to be broken anyway), is there still a reason to not align [[CanPut]] as Mark suggests?
TC39(and the web) has a low tolerance for changes that will break existing code. In general, TC39 tries to avoid changes to the observable semantics of the language as defined by previous versions unless those changes are tied to new syntactic forms or some other type of explicit opt-in. This was true for ES5 and remains true today. Changing the semantics of assignment to an inherited read-only property would be such a change. It wasn't made in E5 and it would seem even harder to justify the change for ES6, given that ES5 made it easier to observe the semantics.
For the record, at the last JSConf, someone showed me code he wrote where he was declaring data attributes on the prototype with 'null' as value. It was the only way for his analysis tool to understand that "child instances" had such and such property. Such a practice prevents freezing the prototype (because the constructor would fail to assign values (unless using Object.defineProperty)). I don't know whether any part of this experience is good or bad, but I think it's worth noting that "declaring" things on the prototype is a practice that exists.
Yes, of course. And in ES5, using Object.defineProperty in the instance constructor is probably the right way to deal with this idiom.
But the above can also be viewed as an existence proof that the existing set of attributes (plus the extensible flag) is insufficient to express all reasonable usage patterns. Rather than changing the semantics associated with an existing attribute to enable a new pattern (while disabling other currently support patterns) another approach to consider is the addition of new but backwards compatible attributes that enable new semantics. For example, you can imagine a "duplicate in children" attributes or a ""writable via over-ride" attribute that would address such scenarios.
In ES5 we avoided new attributes by evolving dontDelete into configurable and in ES6 we want to minimize additional primitive object model changes. However, if there are important use cases that need to be supported then new attribute combinations seem like a plausible alternative to changing existing semantics.
Le 15/01/2012 20:21, Allen Wirfs-Brock a écrit :
On Jan 15, 2012, at 8:10 AM, David Bruant wrote:
Hi,
I'm refering to Allen's writing at [1]:
The basic idea is that the properties prototype object are shared parts of all of inheriting child object. Modifying such a shared part by a child, introduces a local change that is visible to that child (and its children) so this requires creation of a “own” property on the child. However, read-only properties can not modified (by normal means, eg assignment) so there is no need to create a “own” copy.
I agree with what you describe as an intention, but I don't know to what extent it applies to JavaScript (I don't know Self unfortunately):
var p = {}; var o = Object.create(p); var o2 = Object.create(p); o.a = 1; // creating an own property
Object.defineProperty(p, 'a', {value:2, configurable:false, writable:false}); // now, o and o2 "share" an 'a' property. But do they really?
The dynamicity of JavaScript makes the notion of "sharing" dynamic as well, I think. However, the above requires Object.defineProperty which did not exist prior to ES5. Since the starting point of this conversations whether or a design "mistake" was made in the ES5, I don't think it is valid to use defineProperty in trying to understand the original (prior to ES5) design intent.
Here, I used Object.defineProperty, only to illustrate the use of some mechanism that allows the creation of an own read-only property (and not as the standard Object.defineProperty function), sorry for the confusion. Such a mecanism was introduced in ES5 to give authors the ability to define things like the "length" property of a function.
So, in the design of ES5 was planned to add something that offer the capacity to create own properties with fine-grained control. You can replace the above Object.defineProperty call by any other mecanism that would have allowed to dynamically (after object initialization) add an own read-only property since that was a feature that was planned to be added to the language in a way or another regardless of design details (like how the property works when inherited).
Assigning to an inherited read-only property or a “own” read-only property should have the same affect (whether it is ignoring the assignment, throwing, etc.).
// following above code o.a = 3; o2.a = 4;
Due to o having an 'a' property before inheriting from a read-only one, the notion of "same effect" cannot be applied (unless breaking other invariants).
Allowing assignment to an inherited read-only property would break the invariant that that a prototype’s readonly property is an immutable value that is shared among all children of the prototype.
If there was a mistake in designing ES5, it was allowing Object.defineOwnProperty to create child properties that over-ride inherited read-only data properties.This broke an invariant that previously existed in the language but this invariant was already violated by some pre-ES5 clause 15 objects, (eg the writability of the prototype property of some children of Function.prototype). However, I think the ES5 decision was probably the right one given the legacy clause 15 usages and the overall reflective nature of defineOwnProperty).
If I sum up, the design of Object.defineProperty was necessary for backward compatibility (pre-ES5) reasons. A consequence of this design is breaking the invariant you mentionned ("a prototype’s readonly property is an immutable value that is shared among all children of the prototype"). It was already broken in ES3 for some library objects. However, the invariant still held in ES3 for all user defined objects. Object.defineProperty allows ES programmer to "break" the invariant in the same manner that ES implementors had previous been allowed to. However, ES5 maintained the exact same semantics for assignment to an inherited read-only property as had existed in ES1-3.
What is a real world ECMAScript 3 case of inheriting a read-only property? The only code I can come up with is:
function C(){ this.length = 3; }
function f(a,b){}
C.prototype = f;
var c = new C(); console.log(c.length);
For this code, ECMAScript 5 says: When doing "new C()", |this| is assigned "C.prototype" which is the function f (it has a non-writable, non-configurable "length" property with 2 as value). During "this.length = 3;", [[CanPut]] should return "inherited.[[Writable]]" (step 8.b). In this case, it should return false.
Firefox 9, 11 (Aurora) and Chrome 16 log 3 (non-compliant) Opera 11.60 and 12Alpha log 2 (ES5 compliant) Could anyone try with other browsers and post the results, please. So it seems that some major browsers are not conformant to begin with. Interesting.
Can you think of other cases that predate ES5 where browsers acted according to the spec with regard to inherited non-writable properties?
Considering this invariant is broken (as i showed above, it was probably doomed to be broken anyway), is there still a reason to not align [[CanPut]] as Mark suggests? TC39(and the web) has a low tolerance for changes that will break existing code. In general, TC39 tries to avoid changes to the observable semantics of the language as defined by previous versions unless those changes are tied to new syntactic forms or some other type of explicit opt-in. This was true for ES5 and remains true today. Changing the semantics of assignment to an inherited read-only property would be such a change. It wasn't made in E5 and it would seem even harder to justify the change for ES6, given that ES5 made it easier to observe the semantics.
var o = Object.create(Object.freeze({a:1})); o.a = 2; console.log(o.a); // 1 in Firefox (spec compliant), 2 in Chrome
Mark mentionned in the strawman, that it would be 2 in Safari/JSC as well (I can't test now). I haven't heard of anyone neither using this intensively nor even noticing the difference. I fully agree with the goal of preventing inconsistence between the spec and code on the web, but 2 major browsers already have inconsistent behaviors and few seems to have noticed.
Moreover, as I showed above, some major browsers are non-compliant with the spec. Since how long has it been the case?
For the record, at the last JSConf, someone showed me code he wrote where he was declaring data attributes on the prototype with 'null' as value. It was the only way for his analysis tool to understand that "child instances" had such and such property. Such a practice prevents freezing the prototype (because the constructor would fail to assign values (unless using Object.defineProperty)). I don't know whether any part of this experience is good or bad, but I think it's worth noting that "declaring" things on the prototype is a practice that exists. Yes, of course. And in ES5, using Object.defineProperty in the instance constructor is probably the right way to deal with this idiom.
But the above can also be viewed as an existence proof that the existing set of attributes (plus the extensible flag) is insufficient to express all reasonable usage patterns. Rather than changing the semantics associated with an existing attribute to enable a new pattern (while disabling other currently support patterns) another approach to consider is the addition of new but backwards compatible attributes that enable new semantics. For example, you can imagine a "duplicate in children" attributes or a ""writable via over-ride" attribute that would address such scenarios.
What would be the point of duplicating if there is already inheritance. Doesn't inheritance exist to avoid such duplication?
In ES5 we avoided new attributes by evolving dontDelete into configurable and in ES6 we want to minimize additional primitive object model changes. However, if there are important use cases that need to be supported then new attribute combinations seem like a plausible alternative to changing existing semantics.
Good to know. Thanks.
Le 15/01/2012 21:23, David Bruant a écrit :
function C(){ this.length = 3; }
function f(a,b){}
C.prototype = f;
var c = new C(); console.log(c.length);
For this code, ECMAScript 5 says: When doing "new C()", |this| is assigned "C.prototype" which is the function f (it has a non-writable, non-configurable "length" property with 2 as value). During "this.length = 3;", [[CanPut]] should return "inherited.[[Writable]]" (step 8.b). In this case, it should return false.
Firefox 9, 11 (Aurora) (...) log 3 (non-compliant)
Apparently, Firefox logs 2, my mistake.
Le 15/01/2012 21:23, David Bruant a écrit :
function C(){ this.length = 3; }
function f(a,b){}
C.prototype = f;
var c = new C(); console.log(c.length);
For this code, ECMAScript 5 says: When doing "new C()", |this| is assigned "C.prototype" which is the function f (it has a non-writable, non-configurable "length" property with 2 as value). During "this.length = 3;", [[CanPut]] should return "inherited.[[Writable]]" (step 8.b). In this case, it should return false and log 2
So in the "2", we have: IE8, Firefox 9->12, Opera 11.60, 12
and on the side of "3", we have: Chrome 16, Safari 5 and Safari 5.1
Anyway, it seems that the divergence toward this aspect of the spec was probably there even before ES5. If no one complained about it for years it seems to indicate that there is room to change the spec without breaking content, since content does not seem to rely on the spec behavior, even if it was there since ES1.
On Sun, Jan 15, 2012 at 3:02 PM, David Bruant <bruant.d at gmail.com> wrote:
So in the "2", we have: IE8, Firefox 9->12, Opera 11.60, 12
and on the side of "3", we have: Chrome 16, Safari 5 and Safari 5.1
Anyway, it seems that the divergence toward this aspect of the spec was probably there even before ES5. If no one complained about it for years it seems to indicate that there is room to change the spec without breaking content, since content does not seem to rely on the spec behavior, even if it was there since ES1.
I hear from the v8 team that v8 had the current non-standard (and IMO more sensible) behavior back in the ES3 days, and no one noticed. I suspect this was true of Safari as well but have not checked. Chrome + Safari form a large enough share of browsers that the "no one noticed" reaction seems like rather solid evidence to me. We have made other technically non-backward compatible changes from previous specs on web-compat evidence far weaker than this -- and been bitten by very few of these.
I'm refering to Allen's writing at [1]:
I agree with what you describe as an intention, but I don't know to what extent it applies to JavaScript (I don't know Self unfortunately):
var p = {}; var o = Object.create(p); var o2 = Object.create(p); o.a = 1; // creating an own property
Object.defineProperty(p, 'a', {value:2, configurable:false, writable:false}); // now, o and o2 "share" an 'a' property. But do they really?
The dynamicity of JavaScript makes the notion of "sharing" dynamic as well, I think.
// following above code o.a = 3; o2.a = 4;
Due to o having an 'a' property before inheriting from a read-only one, the notion of "same effect" cannot be applied (unless breaking other invariants).
If I sum up, the design of Object.defineProperty was necessary for backward compatibility (pre-ES5) reasons. A consequence of this design is breaking the invariant you mentionned ("a prototype’s readonly property is an immutable value that is shared among all children of the prototype"). Considering this invariant is broken (as i showed above, it was probably doomed to be broken anyway), is there still a reason to not align [[CanPut]] as Mark suggests?
For the record, at the last JSConf, someone showed me code he wrote where he was declaring data attributes on the prototype with 'null' as value. It was the only way for his analysis tool to understand that "child instances" had such and such property. Such a practice prevents freezing the prototype (because the constructor would fail to assign values (unless using Object.defineProperty)). I don't know whether any part of this experience is good or bad, but I think it's worth noting that "declaring" things on the prototype is a practice that exists.
David
[1] strawman:fixing_override_mistake