defineProperty/getProperty design sketch
I'm confused as to why an API is being proposed which clashes with existing JavaScript-style APIs. The one case that I had seen previously, at least related to the implementation within Mozilla, is that it would look something like:
Object.defineProperty(obj, name, value, Object.NOT_WRITABLE | Object.NOT_ITERABLE | Object.NOT_DELETABLE)
Which makes much more sense than the proposal (not forcing the user to create temporary objects just to insert values).
We're tracking this issue for implementation in the next version of Firefox: bugzilla.mozilla.org/show_bug.cgi?id=430133
On Apr 23, 2008, at 8:36 AM, John Resig wrote:
I'm confused as to why an API is being proposed which clashes with
existing JavaScript-style APIs. The one case that I had seen
previously, at least related to the implementation within Mozilla,
is that it would look something like:Object.defineProperty(obj, name, value, Object.NOT_WRITABLE | Object.NOT_ITERABLE | Object.NOT_DELETABLE)
Which makes much more sense than the proposal (not forcing the user
to create temporary objects just to insert values).
I agree it's better to have minimal-cost APIs that do not require
object allocations (or fancy optimizations to avoid allocations).
One overloaded API with an object parameter with too many degrees of
freedom is also undesirable compared to targeted APIs, if the names
and differences in use-case among the targeted set clearly
distinguish them from one another.
Another point (and I've said this to Allen already): I think browser
vendors who have had to implement defineGetter, defineSetter,
lookupGetter, and lookupSetter would rather either
standardize those as-is, or standardize Object.defineGetter, etc.,
counterparts to which the Object.prototype methods could forward.
The clarity of object initialisers for flags is better, but only
slightly. Per Neil Mix's suggestion (in his March 13, 2008 message to
es4-discuss), the Object "static constants" would be named to avoid
double-negative English hazards: WRITABLE, ENUMERABLE, REMOVABLE.
A slight improvement to bias for conciseness in the common case, if
it is the common case, would invert the sense of WRITABLE and require
a READONLY flag for consts. An inverted sense for REMOVABLE would be
named PERMANENT, if that is likely to be the exceptional case (is it?).
This brings up the point Tucker made recently: READONLY with
REMOVABLE is pointless. The Wheat prototype-based programming
language found its initially-Unix-like mode bits had fewer sane than
total linear combinations, too.
StrawName | READONLY ENUMERABLE REMOVABLE ------------+--------------------------------- FIXTURE | 0 0 0 PROTOTYPE | 0 0 1 VAR | 0 1 0 PROP | 0 1 1 CONST | 1 0 0 N/A | 1 0 1 ENUM | 1 1 0 N/A | 1 1 1
The StrawName column contains some proposed names for the
combinations or N/A where the combination is nonsense. These flag-bit-
combination names do not always add clarity compared to |'ed flag-bit
manifest constant names, IMHO.
Comments? We need to come to agreement here quickly, for both ES3.1
and ES4 to hang together.
On 2008-04-23, at 18:35 EDT, Brendan Eich wrote:
The StrawName column contains some proposed names for the combinations or N/A where the combination is nonsense. These flag-bit- combination names do not always add clarity compared to |'ed flag-bit manifest constant names, IMHO.
I have to admit, those don't clarify it for me either. I'm trying to
understand: what would I say in a class attribute declaration to get
the corresponding properties. If the name were mnemonic for that, it
might help. If there is no way to get the corresponding properties
with a class attribute declaration, why would we support doing so
dynamically?
On Apr 23, 2008, at 4:56 PM, P T Withington wrote:
On 2008-04-23, at 18:35 EDT, Brendan Eich wrote:
The StrawName column contains some proposed names for the combinations or N/A where the combination is nonsense. These flag- bit- combination names do not always add clarity compared to |'ed flag-bit manifest constant names, IMHO.
I have to admit, those don't clarify it for me either. I'm trying to understand: what would I say in a class attribute declaration to get the corresponding properties.
The names were based, where possible, on class declaration syntax:
StrawName | READONLY ENUMERABLE REMOVABLE ------------+--------------------------------- FIXTURE | 0 0 0 PROTOTYPE | 0 0 1 VAR | 0 1 0 PROP | 0 1 1 CONST | 1 0 0 N/A | 1 0 1 ENUM | 1 1 0 N/A | 1 1 1
In ES4,
class C { var x; function f() ...; }
make FIXTUREs. For compatibility with ES1-3, you can declare a method
to be prototype:
prototype function charAt(pos)
string.charAt(this.toString(), pos);
(this is from the RI), and it will be defined on the class prototype
(String.prototype in this case, shared with string.prototype) as
REMOVABLE, since you can do this:
js> String.prototype.charAtfunction charAt() { [native code]}js>
delete String.prototype.charAttruejs> String.prototype.charAt js>
in ES1-3.
My choice of name for VAR is confusing since you can use var in a
class to make a fixture, but the reference is to global variables:
js> var x = 1
js> for (i in this)print(i)
x js> delete x
false
and functions too:
js> function f(){}
js> for(i in this)print(i)
f js> delete f
false
These are indeed not REMOVABLE, but they are ENUMERABLE, per ES1-3.
PROP is my cheesy name for a plain old ad-hoc property in ES1-3,
which is ENUMERABLE, REMOVABLE, and not READONLY. CONST is not
READONLY or REMOVABLE. ENUM is CONST with ENUMERABLE.
If the name were mnemonic for that, it might help.
Let me know if you have better name suggestions, based on the above
(please, no EXPANDO for PROP or I'll do something dire :-/).
If there is no way to get the corresponding properties with a class attribute declaration, why would we support doing so dynamically?
The combinations arise in different places in the existing language.
Classes define nominal types, so they are not meant to model all of
the language, in particular PROP additions to dynamic class instances
(such as good ol' Object instances) have no declarative syntax. The
object initialiser syntax is sugar, not declarative in the same sense
as class syntax -- it desugars to property "sets" or "[[Put]]" calls
to use ES1-3's meta-method, and that checks for readonly prototype
properties.
The short answer is that we have a core ES1-3 language for creating
global PROTOTYPE, VAR, and PROP attribute-sets, but users of the
language can't declare PROTOTYPE -- only the magic builtins get to
make "DontEnum" properties in ES1.3. ES4 supports FIXTURE and CONST
declarations.
I had not thought of an example of ENUM in ES4, but I believe this is
one:
obj = {const FOO: 42}.
FOO is not CONST because it should be enumerable by for (i in obj)
constructs.
ES4 purists might object that the FOO initialiser makes a fixture,
but not a FIXTURE -- but if the game here is to name all sane
attribute bit combinations, then it's hard to win with short names
and avoid dropping FIXTURE from all the fixture variants. So I went
with ENUM, and that name could be expanded to ENUMERABLE_CONST where
CONST implies "fixture". So we would have long-winded
ENUMERABLE_CONST_FIXTURE, CONST_FIXTURE, GLOBAL_VAR, etc. names
(instead of ENUM, CONST, and VAR respectively). But anything so long-
winded loses to the disjunction of flag bit constants.
On Apr 23, 2008, at 5:25 PM, Brendan Eich wrote:
For compatibility with ES1-3, you can declare a method to be prototype:
prototype function charAt(pos) string.charAt(this.toString(), pos);
(this is from the RI), and it will be defined on the class prototype (String.prototype in this case, shared with string.prototype) as REMOVABLE, since you can do this:
js> String.prototype.charAtfunction charAt() { [native code]}js> delete String.prototype.charAttruejs>
String.prototype.charAt js>
Ugh, Mail.app in plain text mode dropped newlines from the paste there:
js> String.prototype.charAt
function charAt() { [native code] } js> delete String.prototype.charAt
true js> String.prototype.charAt
js>
... we would have long-winded ENUMERABLE_CONST_FIXTURE, CONST_FIXTURE, GLOBAL_VAR, etc. names (instead of ENUM, CONST, and VAR respectively). But anything so long- winded loses to the disjunction of flag bit constants.
I hope this is a convincing argument against naming attribute
combinations. Bit flags or equivalent (boolean-initialized properties
in Allen's proposal) win, because defining a property is not the same
as declaring using special syntax, or assigning using an assignment
expression. The "gesture" has no composite name drawn from the rest
of the language, in a general and easy to understand way.
I don't think this is a sign of some deep flaw, although it could be
used to argue for a larger set of narrower APIs: defineConstant,
defineFixture, etc. -- but those would be too numerous, and
enumerability is still mostly orthogonal to the READONLY without
REMOVABLE vs. writable and removable or permanent combinations. So
there'd still be an enumerable flag bit.
Still worth considering, given the unwanted two states (READONLY +REMOVABLE) in the table. Thanks for provoking this thread.
On 4/23/08, Mark S. Miller <erights at google.com> wrote:
Use case: modify attributes of an existing property.
Descriptor = { name: string, [, writable: boolean] [, enumerable: boolean] [, deletable: boolean]} Without the "name: string" part, since that comes from the multiDescriptor.
If obj has a local property whose name is the value of the name element of the descriptor set the attributes of the property according to the values of the writable, enumerable, and deletable elements of the descriptor. Fail if the property does not exist (or add fixed/frozen attribute) If the property does not yet exist, then all unenumerated attributes of an explicitly defined property should default to false. If the property already exists, then unenumerated attributes should default to their existing value.
Are there compelling use cases for modifying the writable and deletable attributes of a property, post-definition? If a non-writable property can be made writable, then all properties are writable. Some are just more annoying to write to than others. Does Object.freeze have an irreversible effect on the deletable attribute of the object's properties? (Same question for Object.seal / writable...)
Even if the answer is yes, this still isn't sufficient to express the ES4 notion of a fixture, since an entire object is frozen/sealed, whereas in ES4:
var o = {var x: 10, y: 20};
... my understanding is that o.x cannot be deleted by any mechanism, since part of the intention behind fixtures is to allow early binding.
Maybe the idea is that an attempt to make o.x deletable would simply fail in ES4. But since Brendan noted that initializers should be considered sugar for imperative code, we should be able to describe the desugaring -- preferably as a plain source-to-source transformation that doesn't require reference to some, say, [[SetAttribute]] meta-method. That would require an imperative way of defining a fixture (in the ES4 sense). Maybe that could be done as an extension to what's being proposed for ES3.1, rather than as part of it, but I don't really know what that would look like.
On Wed, Apr 23, 2008 at 8:36 AM, John Resig <jresig at mozilla.com> wrote:
I'm confused as to why an API is being proposed which clashes with existing JavaScript-style APIs. The one case that I had seen previously, at least related to the implementation within Mozilla, is that it would look something like:
Object.defineProperty(obj, name, value, Object.NOT_WRITABLE | Object.NOT_ITERABLE | Object.NOT_DELETABLE)
Which makes much more sense than the proposal (not forcing the user to create temporary objects just to insert values).
Hi John,
My impression had been that the bit-field API style, which I had previously advocated, was also rejected on the same grounds: that it conflicts with JS style APIs. Instead, people were talking about a bunch of positional boolean flag parameters, which is a call-site readability disaster:
Object.defineProperty(obj, name, value, true, false, true)
Say what?
I do find the descriptor proposals more readable than the bit-field proposals. Also, the descriptor proposals can more naturally distinguish beween true, false, and left-out-so-use-default. There's no pleasant enough way to do three valued logic with bit fields.
Allen and Crock, Check out the bottom of mail.mozilla.org/pipermail/es4-discuss/2008-April/date.html.
Your posts are still not showing up on es4-discuss; I'm not sure why.
Everyone else, if you want to see the missing posts, please check out mail.mozilla.org/pipermail/es3.x-discuss/2008-April/date.html.
On Wed, Apr 23, 2008 at 7:09 PM, Allen Wirfs-Brock <Allen.Wirfs-Brock at microsoft.com> wrote:
I think our differences are largely syntactic sugar, but that sugar can be important for usability.
Agreed.
If I interpret you comments correctly you are proposing to only have the defineProperties form with the multi-descriptor. Is that correct?
Yes.
In that case my first example would look like:
Object.defineProperties(Array.prototype,{some: {enumerable:false}});
Rather than:
Object.defineProperty(Array.prototype, {name: "some", enumerable: false});
yes.
And modifying multiple properties would look like:
Object.defineProperties(Array.prototype,{ some: {enumerable:false}, map: {enumerable:false}, foreach: {enumerable:false}});
rather than: [...]
yes
I could go either way, for a single property the extra level of { } seems unnecessary but getting rid of the quoted name is nice.
Notationally, an extra {} is cheaper.
My bigger concern is what your intend to do about the name property of individual descriptors.
Kill it.
Do you ever have one, particularly for a descriptor returned from getProperty/getProperties?
No.
Or is an outer multidescriptor always used to name a descriptor?
Yes
Is it a good idea to allow for loose descriptors without an associated name?
I don't see why not.
Regarding: Object.defineProperties(obj, {x: {value: 0}, y: {value: 0}, {pi: {writable: false}});
Since you assert that "all unenumerated attributes of an explicitly defined property should default to false" then it would seem that you should have said:
Object.defineProperties(obj, {x: {value: 0, writable: true }, y: {value: 0, writable: true }, {pi: {writable: false}});
Depends on what the intent was. Given the intent you imply, then yes. If "pi" is known to be a new property, then "{pi: {value: 3.14159}}" would be sufficient.
This is part of the reason, that I distinguished between a descriptor containing "value:" and a descriptor containing "method:". My thinking was that the "normal" writability of an "instance variable" property would be true while for a property used as a "method" the normal writability would be false. I wanted to make this normal usage the default. In addition, in this semi-declarative style of object definition, I think it is a good idea to allow the user to be explicit about which properties are intended to be variables and which are intended to be properties.
You mean "methods" rather than "properties" at the end above?
If the only operational effect of distinguishing value: vs method: is these defaults, I don't think the benefit pays for the additional complexity. I'd rather adopt the "deny by default" common wisdom. Now that we're consistently phrasing attributes in terms of the things they allow, having them default to false, i.e., deny, is easy to remember.
BTW, this may be an issue for the getter/setter specification but it's not clear to me what the interpretation of the ReadOnly (ie writable) attribute should be for getter/setter properties. Should it indicate whether a getter is defined or does it control whether or not the property can be redefined to not be a getter/setter. To consistently add getters/setters and allow for their transparent use do we actually need a new attribute which means not redefinable?
A good question. Perhaps "deletable" should be subsumed by "redefinable"?
Bottom line, I think your multi-descriptor form could grow on me.
Glad to hear it! Though I'll try to avoid visualizing a multiDescriptor growing on you ;).
I'm concerned about not having a name property in a naked descriptor.
Just use a singleton multiDescriptor instead to fully describe a single property.
I like distinguishing intent about methods and instance variable property usage and that may require that the default interpretation of "unenumerated properties" needs to be situational.
I wish to minimize situational caveats in the spec. Let's keep it simple.
On Wed, Apr 23, 2008 at 2:37 PM, Douglas Crockford <douglas at crockford.com> wrote:
I am very encouraged by this. We are clearly on the right path here. We are exposing essential mechanisms with a light and expressive interface. Most new feature proposals barely meet the standard of usefulness. This one meets a much higher standard, that of necessity.
There are just the details to get right. In comparing Allen's sketch and Mark's counter sketch, it seems to me that either would work.
I think I refer Allen's. I like the symmetry of the structure shared between Object.setProperty and Object.getProperty.
The other proposal has a similar symmetry between defineProperties and getProperties. Both traffic only in multiDescriptors.
I like the finer granularity of -y() rather than -ies() because it is clearer what happens if a request fails. Object.setProperties can set many attributes of many properties. If setting of a single attribute fails, in what state is the object left? No changes, partial changes, all possible changes?
Great question! No change. As a result, the -ies() forms provide functionality not practically achievable with the -y() form.
In the case where it fails, I prefer return false to throw. It doesn't seem extraordinary enough to warrant an exception.
Defensive code would then always have to check the return value and throw if it's false. As Allen's own expository examples show, and as a long history of bad Unix/C programs show, programmers will often forget to do this, rendering their code vulnerable to confusion attacks. Once something goes wrong, especially if an attacker can induce the failure, execution must not innocently proceed on normal flow-of-control paths whose assumptions are violated.
I want to understand why Object.beget needs more than one parameter.
I think I get it:
Object.beget(foo, {x: 3, y: 4})
would be like
{__proto__: foo, x: 3, y: 4}
Instead, people were talking about a bunch of positional boolean flag parameters, which is a call-site readability disaster:
Object.defineProperty(obj, name, value, true, false, true)
Just pointing out that this would be self-documenting (with the same performance) with named parameters:
Object.defineProperty(obj, name, value, writable: true, iterable: false, deletable: true)
Which may be an option in future versions.
The following are comments I sent Allen on a slightly earlier draft. I think all these are still applicable.
---------- Forwarded message ---------- From: Mark S. Miller <erights at google.com>
Date: Tue, Apr 22, 2008 at 4:32 PM Subject: Re: define/getProperty meta operations To: Allen Wirfs-Brock <Allen.Wirfs-Brock at microsoft.com>
*Problem: Design Pattern for Supporting Meta Operations Upon Objects --
Hi Allen,
As discussed on the phone, I like a lot of this! However, I would like to use a nested object literal to describe a set of properties, and I want to avoid using a function's declared name as a way to cutely specify the corresponding property name. Below each of your use cases, I'll show the alternative I have in mind.
Object.defineProperties(<obj>, <multiDescriptor>)
<multiDescriptor> = { (<propertyName>: <descriptor>)* }
throws if not successful.
Without the "name: string" part, since that comes from the multiDescriptor.
If the property does not yet exist, then all unenumerated attributes of an explicitly defined property should default to false. If the property already exists, then unenumerated attributes should default to their existing value.
Object.defineProperties(Array.prototype, {some: {enumerable: false}});
Object.defineProperties(obj, {x: {value: 0}, y: {value: 0}, {pi: {writable: false}});
Unenumerated attributes of newly defined properties should always default to false. Are there other reasons to distinguish methods from values? I will proceed for now assuming not.
Object.defineProperties(obj, {doSomeWork: {value: function() {this.work();}}});
But without the "name: string" part again.
Whoa! In ES3, all property names are strings. Above, you seem to suggest allowing first class anon object refs as keys. This is eventually attractive on many grounds, but is way too big a step for ES3.1. Below, I will assume instead the property name "privateprop_y", which, of course, isn't private at all.
Object.defineProperties(obj, { privateprop_y: {value: 0, writable: true}, y: {getter: function() {return this.privateprop_y;}, setter: function(arg) {this.privateprop_y = arg;}} });
Object.getProperties(obj) => <multiDescriptor>
As if defined by
Object.getProperty = function(obj, name) { return Object.getProperties(obj)[name]; };
as well as Object.seal(obj) which seals without freezing. A sealed object is non-dynamic -- new properties cannot be added to it, and its existing properties become {deletable: false}. A frozen object is a sealed object with the additional constraint that all its existing properties become {writable: false}.
var fixture = new Object(); Object.defineProperties(fixture, { method1: {value: function(...){...}}, method2: {value: function(...){...}}, prop3: {getter: function(){...}, setter: function(arg){...}} }); Object.seal(fixture); // for the last line above, is this what you meant?
I still don't like the idea of extending beget with copying behavior.