Here is the second draft, which incorporates most things discussed so far. Changelog near the beginning, with an OPEN ISSUES section.
--larsTitle: Object literals
Object initializer syntax
NAME: "Object initializer syntax"
FILE: spec/language/object-literals.html
CATEGORY: Expressions (E262-3 chapter 11)
SOURCES: ES3; REFERENCES [1]-[7]
SPEC AUTHOR: Lars
DRAFT STATUS: DRAFT 2 - 2008-04-07
REVIEWED AGAINST ES3: YES
REVIEWED AGAINST ERRATA: YES
REVIEWED AGAINST BASE DOC: YES
REVIEWED AGAINST PROPOSALS: YES
REVIEWED AGAINST CODE: NO
REVIEWED AGAINST TICKETS: YES
IMPLEMENTATION STATUS: ?
TEST CASE STATUS: ?
CHANGES SINCE DRAFT 1 (2008-03-20)
* Added optional 'const' and 'var' prefixes to the initializer to
imply 'const' or 'var' for all fields.
* Specified that a 'const' or 'var' prefix on a field records the
type of the value being stored in the type of the object, absent
any other annotation; the previous draft used '*' for the types.
* Specified that repeated field names are allowed only if the
initializer like a whole does not use any new ES4 features
* Introduced catch-all methods 'meta::get' and so on
* Introduced 'meta::prototype'
* Added the facility described in [6] for annotating the initializer
with a nominal class type.
* Added "Open issues" section; one more reference; wording changes.
OPEN ISSUES
* There is no way to control enumerability of a property without
giving it a non-public namespace or making it a fixture. One
possibility without adding yet another keyword is to signal
non-enumerability by the explicit use of the 'public' namespace:
{ public::x: 10 } /* not enumerable */
It's unambiguous but feels a little hackish, since the rule
for enumerability is that public properties are enumerable.
* There is no way to make getters or setters fixtures without
resorting to a type annotation, because they can't be
designated 'var' or 'const'. I don't know if this is a
hardship but the spec seems incomplete without it. One
possibility is to allow 'var' to prefix 'get' and 'set'
(using 'const' would be wrong as there is always a setter).
REFERENCES
[1] ES4 base document
[2] Ticket #164
[3] Ticket #165
[4] Ticket #219
[5] Ticket #319
[6] Ticket #370
[7] Bug fixes proposal, item about comma at the end of the field list
Synopsis
This draft spec tries to pin down everything that has been proposed and tentatively agreed about object initializer syntax and semantics. A brief rationale is attached at the end.
Primary syntax
In its general form an object initializer is comprised of an
optional keyword (const or var), followed by a brace-delimited
comma-separated list of fields with the last field optionally followed
by a comma, followed by an optional type annotation.
ObjInit ::= ["const" | "var"] "{" ( ( Field "," )* Field ","? )? "}" [ ":" Type ]
Field ::= FieldName ":" AssignmentExpression
| "var" FieldName ":" AssignmentExpression
| "const" FieldName ":" AssignmentExpression
| "get" FieldName "(" ")" [":" Type] FunctionBody
| "set" FieldName "(" Param ")" [ ":" "void" ] FunctionBody
| "meta" "::" "prototype" ":" AssignmentExpression
| "meta" "::" "get" "(" Param ")" [":" Type] FunctionBody
| "meta" "::" "set" "(" Param "," Param ")" [ ":" "void" ] FunctionBody
| "meta" "::" "has" "(" Param ")" [":" "boolean"] FunctionBody
| "meta" "::" "delete" "(" Param ")" [ ":" "void" ] FunctionBody
| "meta" "::" "invoke" "(" ( Param ( "," Param )* )? ")" [ ":" Type ] FunctionBody
FieldName ::= AnyIdentifier | AnyIdentifier "::" AnyIdentifier | LiteralString | LiteralNumber
AnyIdentifier ::= Identifier | ReservedWord
If a FieldName has a qualifier then the qualifier must name a
binding created by a namespace directive and it the qualifier
cannot be the name of a reserved namespace (meta, intrinsic,
reflect, and so on).
The "Type" that annotates the initializer must be a record type or a class type whose constructor accepts zero arguments.
The FunctionBody of a getter, meta::get, meta::has,
and meta::invoke can be a block or an _expression_. The
FunctionBody of a setter, meta::set, and meta::delete, can
only be a block (until we decide that : void on an _expression_
closure transforms the value computed to void before "returning").
It is possible to have a getter without a setter and a setter without a getter. A compatible getter or setter will be generated for the missing method. The generated setter method receives a value and discards it silently (this corresponds with the view that writing to ReadOnly properties fails silently). The generated getter method throws a ReferenceError.
If the initializer is prefixed by const or var then
const or var is implied for each of the fields in the
structure. In this case, all the fields must be simple
fieldname:value fields, and none of them must have const or
var annotations.
Field names may be repeated only if the initializer as a whole
looks like an object initializer as defined in E262-3 (1999), ie, all
fields are of the FieldName : AssignmentExpression form,
there is no const or var qualifier on the initializer as a
whole, and there is no type annotation on the initializer.
Construction
Unlike the case in ES3, the program can't shadow the binding for
Object in order to invoke an alternative object constructor for
object initializers.
If a type annotates the initializer and that type is a class type then the object initializer syntax is shorthand for the creation of an instance of that type with assignments to properties of the fields of that instance. In other words, given
class Point { var x, y }
then
{ x: 10, y: 20 } : Point
is shorthand for
(let (TMP = new Point)
(TMP.x = 10,
TMP.y = 20,
TMP))
for some fresh variable TMP. In this case, all fields of the object
literal must be of the FieldName : AssignmentExpression
form. (See the Rationale section for a discussion of why this is
desirable.)
In all other cases, the initializer evaluates to an instance of
Object or an anonymous subtype of Object, as described in the
rest of this document.
Secondary syntax
Suppose T is a structural record type:
type T = { x: int, y: double }
Then the new operator can be used as follows:
new T(10, 2.5)
The meaning of this is precisely:
{ x: 10, y: 2.5 } : T
There must be as many arguments to new as there are fields in
T. The initializers are matched with fields by the order in which
they appear.
Semantics of subphrases
Types and fixtures
If a property name in the record type that annotates the literal
matches a field name in the literal then the field is a fixture (as
opposed to a dynamic property) and the type of the fixture is the type
of the property given in the record type. The following makes x a
fixture and gives it the type int:
{ x: 10 } : { x: int }
The type of the value must be of the type of the field, or must be convertible to the type of the field.
If a literal field is annotated by const or var and the
field is also named in the record type that annotates the literal then
the type of the property is the type given in the record type, not the
type implied by the initial value of the property (see below).
If a property name in the record type matches a field name that is a getter and/or a setter then:
- either the getter has no return type annotation (in which case the type from the record type will be applied to the getter) or the return type must be equal to the type present for the property in the record type;
- either the setter has no parameter type annotation (in which case the type from the record type will be applied to the parameter) or the parameter type must be equal to the type present for the property in the record type; and
- following resolution of the previous two points, the return type of the (generated) getter, the parameter type of the (generated) setter, and the type in the record type must all be equal.
Fields may be present in the field list that are not present in the type, but not vice versa. I.e., the following is legal:
x = { x: 10, y: 20, z: 30 } : { x: int, y: int }
A field that does not have a matching explicit type annotation in the record type is dynamic, which is to say it is deletable. Note in particular that this applies to getters and setters. A getter/setter pair can only be deleted as a unit.
If a field name that has a getter/setter pair is not mentioned in the record type for the object initializer then the getter's return type must be equal to the setter's parameter type.
Namespaces
Fields are in the compatibility namespace noNS if they don't have an explicit qualifier.
NOTE The use default namespace pragma does not apply to object
initializers.
Enumerability
As outlined elsewhere [forthcoming enumerability spec], fixture
properties are never enumerable. Dynamic fields are enumerable if
they are in the namespace noNS and their enumerable bit is
set.
All dynamic fields created by an object initializer have their
enumerable bit set.
const
The const attribute introduces a fixture. The meaning of
{ const x: E }
is the same as the meaning of
{ x: E } : { x: T }
with the additional constraint that the writable bit on x is
disabled (x is ReadOnly in ES3 lingo).
The type T is the manifest type of the value resulting from evaluating E.
var
The var attribute introduces a fixture. The meaning of
{ var x: E }
is exactly the same as
{ x: E } : { x: T }
The type T is the manifest type of the value resulting from evaluating E.
Getters and setters
A getter (get Name()
) must not take any arguments.
A getter must not be declared to return void.
A setter (set Name()
) may be declared as returning
void but must not be declared as returning any other type.
If the program reads a property from an object and that property was named by a getter, then the getter method is invoked and the value returned by the getter method is returned to the program.
If the program writes a property to an object and that property was named by a setter, then the setter method is invoked with the value being written as its only argument. The value returned by the setter method, if any, is discarded.
Inside the getter and setter methods the value of this refers
to the object on which the getter or setter was defined.
Catch-all methods
Catch-all methods (meta::get, meta::set, meta::has,
meta::delete, and meta::invoke) are the values of read-only
fixture properties on the object that is created by the object
initializer _expression_.
NOTE The full catch-all protocol is described elsewhere. Here is a summary.
Some catch-all methods are invoked when an object
property is accessed by a primitive protocol and the property is not a
fixture of the object. In ES3 terms, meta::get is invoked by
[[Get]], meta::set is invoked by [[Put]]; meta::has is invoked
by [[HasProperty]]; and meta::delete is invoked by [[Delete]].
These four catch-all methods receive the name of the property being
accessed as the first argument (currently encoded as a Name, a string,
or a nonnegative integer value below 232-1).
The catch-all methods handling properties are always invoked, even if a property being sought is defined as a dynamic property on the object.
The catch-all method meta::invoke is invoked when
the object is called as a function.
A catch-all method operates on the own object (the
value of this) and can either terminate normally by returning or
else signal to its caller -- by throwing a distinguished exception --
that default behavior should be invoked on the object.
A catch-all field is syntactically distinguished by the use of the
meta namespace. At the time the object initializer is evaluated
the name meta must reference the immutable binding for the
meta namespace in the global object.
Catch-all methods can have type annotations in their parameter and return positions.
Catch-all properties can appear in the record type that annotates the initializer. Any types for catch-all properties in the record type must be equal to the declared types of the catch-all methods in the initializer itself.
Even if not mentioned in the record type that annotates the initializer, catch-all properties have field types derived from the annotations on the catch-all methods.
meta::prototype
The special field name meta::prototype allows a value to be
specified for the internal [[Prototype]] object of the newly
constructed object.
The value for meta::prototype must not be undefined or null.
As for catch-all methods, this field syntactically distinguished
by the use of the meta namespace. At the time the object
initializer is evaluated the name meta must reference the
immutable binding for the meta namespace in the global object.
The syntax meta::prototype exists for initializing the
[[Prototype]] object only; it is not available for reading the
[[Prototype]] value from the object in any context.
Rationale
(Will not be part of the final spec.)
The provision for an optional trailing comma comes from an early bug fix proposal. Everyone wants this.
Getters and setters have found a lot of use on the web and are a much-desired feature, even the ES3.1 group has been debating it. They are implemented in the form presented here in Firefox and Opera, at least.
const fields are motivated by the practical need to protect
some object fields from being changed while staying within the easy to
manage world of object initializers (ES4 classes would do the job but
are more heavyweight by far). Structural type annotations cannot
express what const can express, either. Some examples of the use
of const are presented in the paper, "Evolutionary programming and
gradual typing in ECMAScript 4", available from ecmascript.org.
var fields are similar to const fields though in some
cases they can be expressed by structural type annotations. They seem
"nice"; they allow the lightweight introduction of fixtures without
the use of structural types.
Allowing a single const or var to cover all fields in the
initializer simplifies programs.
Inferring types from the initializers for const and var
fields reduces annoying redundancy in some programs, but it is not
without perils: It is easy to construct objects of a more specific
type than was intended. Also, some participants in the discussion
want more than what is provided for here, namely that an object that
is prefixed by const is non-dynamic.
Syntax for catchalls is provided because it's sugar for
functionality that is almost available. However, restrictions on
the use of the meta namespace (which is reserved by the language
implementation) prevent the creation of fields like this:
meta::get: function (name) ...
and it is clearer to provide the functionality directly.
Syntax for setting up the prototype chain is provided because it's a common need and because it provides power that is not available without the new syntax. (ES3 style constructor functions can only create objects of the empty object type; field types cannot be introduced. Yet ES3 style constructor functions are the only other means available to create a custom prototype chain.)
Structural type annotations on object initializers are a convenient shorthand for creating typed fixtures on objects without having to go the roundabout way through full classes. "I want to guarantee that these fields are here and that they have these types." It's lightweight integrity.
The new syntax is part of the evolutionary programming agenda
and is yet another point on the continuum between ES3 programs and
class-based ES4 programs (the syntax abstracts away from the type T
that is the subject of new -- whether it's a class or a structural
type).
Nominal type annotations on object initializers are motivated not
so much by the object initializer case as by the array initializer
case, where [1,2,3]:Vector.<double> is an easy-to-read
syntax that furthers the evolutionary programming agenda.
_______________________________________________ Es4-discuss mailing list [email protected] https://mail.mozilla.org/listinfo/es4-discuss
