ECMAScript 4 Netscape Proposal
Rationale
Miscellaneous
|
Wednesday, June 4, 2003
This section presents a number of miscellaneous alternatives that were considered while developing this proposal.
Object
The root of the type hierarchy was chosen to be the existing ECMAScript 3 type Object
.
Primitive numbers, strings, and booleans were made into instances of subclasses of Object
and the existing wrapper
classes eliminated, thereby eliminating the confusing distinction between boolean primitives and objects, etc. in ECMAScript
3.
The alternative was to define a new root type, any
, that is an ancestor of Object
as well as
primitive numbers, strings, and booleans, which could have their own types number
, string
, and boolean
respectively. In this scenario the ECMAScript 3 classes Number
, String
, and Boolean
would still be subclasses of Object
, but the primitive values could not be members of these classes — for
example, false
would be a member of the class boolean
but not Boolean
. The object obtained
by boxing false
would be a member of the class Boolean
but not boolean
and would evaluate
to true (!) if used in an if
statement.
The any
alternative was more compatible with ECMAScript 3, but the added complexity and confusion was not
deemed worthwhile for the extra compatibility. Instead, ECMAScript 4 simplifies and regularizes the behavior of ECMAScript
3 in this area.
Never
and Void
The types Never
and Void
are different and serve different purposes. When used as a return type, Never
describes the inability to return
from a function, while Void
states that the function returns, but the value returned is not useful (it’s
always undefined
).
The following example illustrates the use of Never
and Void
:
// This function returns no useful value. function display(message:String):Void { document.write("<\_P>" + message + "<\/P>\n"); } // This function cannot return. function abort(message:String):Never { display(message); throw AbortException; } function chickenCount(myChickens:Array[Chicken]):Integer { if (notHatched(myChickens)) abort("Can’t count the chickens yet"); else return myChickens.length; }
Note that if the function abort
had no explicit return type or any return type other than Never
,
then the compiler would likely issue a warning inside the function chickenCount
because it contains a code path
(the false case of its if
) that appears to fall out of the function without returning a value, while chickenCount
is declared to return an Integer
. The Never
return type on abort
tells the compiler
that there is no such code path inside chickenCount
.
It might be a good idea for a compiler to issue a warning for a function that is declared as returning type Never
for which the compiler can’t verify that the function can’t return.
The proposal originally had additional array types which were dropped for simplicity. The following are some candidates for a future revision of the language:
Type | Set of Values |
---|---|
Array |
Same as Array[Object] |
Array[ t] |
null as well as all arrays (dense or sparse) capable of holding elements of type t |
List[ t] |
null as well as all resizeable, dense arrays capable of holding elements of type t |
t[] |
null as well as all nonresizeable, dense arrays capable of holding elements of type t |
ConstArray |
Same as ConstArray[Object] |
ConstArray[ t] |
null as well as all constant arrays capable of holding elements of type t |
A sparse array may have holes. A dense array always has contiguous elements indexed starting from zero. Resizeable arrays
may be resized by writing to the length
property.
When A is one of the type expressions Array[
t]
, List[
t]
,
t[]
, ConstArray
, or ConstArray[
t]
, the following
three operations can be used to create an array of type A or coerce to one:
new
A(
elt1,
elt2,
...)
creates an instance of the corresponding array type with the given elements; this is true even if there is only one element
given.new
A(length:
n)
creates an instance of the corresponding
array type with a length of n. The initial elements are holes if the array is sparse or the default value for
A’s element type if the array is dense. Note that this form requires the named function
parameters extension.(
B)
creates an instance of the corresponding array type. The new instance’s
elements are copied from B, which should be any kind of an array. It’s unresolved what should happen
if A is a dense array and B has holes.Instances of non-ConstArray
array types are equal only when they are the same object. Instances of ConstArray
array types are equal when they have the same contents and the same element type t.
We could define other type operators such as the ones in the table below. s and t are type expressions.
Type | Values | Implicit coercion of value v |
---|---|---|
t[] |
null as well as nonresizable arrays of values of type t |
undefined null |
const t |
Makes type t, which must be an array type, into a read-only array type | None |
- t |
Any value belonging to type t except null |
|
~ t |
undefined or any value belonging to type t |
undefined undefined ;
any other implicit coercions already defined for t |
+ t |
null or any value belonging to type t |
null null ; undefined
null (if undefined is not a
member of t); any other implicit coercions already defined for t |
s + t |
All values belonging to either type s or type t or both | If vs+ t,
then use v; otherwise, if v as s is defined then use v as s;
otherwise, if v as t is defined then use v as t. |
s * t |
All values simultaneously belonging to both type s and type t | If v as s as t is defined
and is a member of s* t, then use v as s as t. |
s / t |
All values belonging to type s but not type t | If v as s is defined and is a member of s/ t,
then use v as s. |
The []
suffix operator could be used to make array types. Unlike Array
, these arrays would be
dense, indexable only by integers, nonresizable, and non-subclassable — t[]
has the class
modifier final
.
A new unary operator, const
, could be added. This operator would
take a PostfixExpressionOrSuper x
as an operand. If x is an instance of a non-ConstArray
array, const
x
would return a ConstArray[
T]
copy of x, where T is the most specific
type such that every element of x is a member of T. If x is an array type, const
x
would be the corresponding ConstArray
type.
[]
is the type of writable arrays of t.const
t[]
is the type of constant arrays of t.[][]
is the type of writable arrays of writable arrays of t.const
t[][]
is the type of constant arrays of writable arrays of t
— const
binds looser than []
so it’s the same as const (
t[])[]
.(const
t[])[]
is the type of writable arrays of constant arrays of t.Earlier ECMAScript 4 proposals allowed a class to have multiple constructors with different names, callable using the
same syntax as calling static functions. These were dropped to keep the language simple. Since now each class can have at
most one constructor, the this(
args)
form for calling another constructor from the same
class was also dropped. Constructors had been defined as described below.
A constructor is a function that creates a new instance of a class C. Constructors are defined using the attribute
constructor
. A constructor is usually given the same name as its class, in which case the constructor
attribute is optional and the constructor is known as a default constructor and can be called as new
C.
If a constructor has a different name, it is invoked as though it were a static
function of the class:
class C {
var a:String;
constructor function C(p:String) {this.a = "New "+p} //
The attribute constructor
is optional here
constructor function make(p:String) {this.a = "Make "+p}
static function obtain(p:String):C {return new C(p)}
}
var c:C = new C("one");
var d:C = C.C("two");
var e:C = C.make("three");
var f:C = C.obtain("four");
c.a; //
Returns "New one"
d.a; //
Returns "New two"
e.a; //
Returns "Make three"
f.a; //
Returns "New four"
A constructor can refer to its class’s instance variables via this
. If a class C inherits
from class B, then when B’s constructor is called while creating an instance of C,
B’s constructor will be able to call virtual methods of class C on the partially constructed instance.
Likewise, B’s constructor could store this
into a global variable v and some other
function could call a method of C on the partially constructed object v. Class C’s
methods can be assured that they are only called on fully initialized instances of C only if neither C
nor any of its ancestors contains a constructor that exhibits either of the behaviors above.
A constructor should not return a value with a return
statement; the newly created object is returned automatically.
A constructor’s return type must be omitted.
A constructor
function always returns a new instance. On the other hand, a static
function can
return an existing instance of its class. It is possible to define a static
function with the same name as its
class C; such a function looks like a constructor to the outside (it can be called as new
C)
but can hand out existing instances of its class. However, subclasses will see that C is not a constructor in such
a class because they will not be able to invoke C’s pseudo-constructor from their constructors.
If a class C does not define the default constructor C.
C, the default constructor
C.
C is automatically defined; that constructor takes the arguments that C’s
superclass’s default constructor B.
B takes. C.
C
calls B.
B and then initializes C’s new instance members to their default
values.
Let C be a class and B its superclass. Any constructor C.
n must
call a constructor B.
m or C.
m before it accesses this
or super
or before it returns. The call can be either explicit or implicit; if C.
n
does not contain any calls to a constructor B.
m or C.
m,
then a call to B.
B with no arguments is automatically inserted as the first statement of
C.
n. The constructor C.
n does not have to call another
constructor if it exits by throwing an exception. C.
n may not make a constructor call more
than once.
A constructor C.
n can call another constructor using one of the following statements:
super( args) |
Calls B’s default constructor B. B. |
super. m( args) |
Calls B’s constructor B. m. m must be a QualifiedIdentifier
that names one of B’s constructors. |
this( args) |
Calls C’s default constructor C. C. |
this. m( args) |
Calls C’s constructor C. m. m must be a QualifiedIdentifier
that names one of C’s constructors. |
The above must be complete statements, not subexpressions of larger expressions. The first of the four forms above is unambiguously
a SuperStatement, but the remaining three are parsed as ExpressionStatements.
The following rules indicate whether one of these three forms (super.
m(
args)
,
this(
args)
, or this.
m(
args)
)
is treated as an expression or a call to a constructor:
this(3,4)+5
), then it is an expression.super.
m(
args)
does not name one of the
superclass’s constructors, then the form is an expression (that, in this case, looks up the superclass’s property
m on this
and invokes it as a method with the arguments args).this.
m(
args)
does not name one of the
current class’s constructors, then the form is an expression (that, in this case, looks up the property m
on this
and invokes it as a method with the arguments args).It is not possible to skip class hierarchy levels while constructing an object — if C’s superclass is B and B’s superclass is A, then one of C’s constructors may not directly call one of A’s constructors.
A class extension is the ability to add methods to a class defined somewhere else. Class extensions were part of the ECMAScript 4 proposal but were dropped to keep the language simple; although useful, they significantly complicated the design of classes and introduced some pesky problems such as what to do when a package P first imports a base class C from package Q, then defines a subclass D of C, and finally extends C with an extension with the same name and namespace as a method of D.
The proposed class extensions were created using the extend
attribute described below.
extend
AttributeThe extend
attribute takes a parameter C, which should be a compile-time
constant expression that evaluates to a class, and adds the definition as a new member of class C. This allows
one to add a method to an existing class C even if C is in an already defined package P.
There are several restrictions:
extend
attribute can only be used on function
, const
, class
,
or namespace
definitions, or on static var
definitions. Thus, a class extension cannot
add new instance variables to objects of class C; it can, however, add getters and setters. Operators
cannot be defined in class extensions.static
or final
, and it cannot override any existing member.
If the new member is a function
, the default is final
.public
, because the public
namespace is predefined by the system
and not by any package. The default namespace is internal
, which makes the new member of C visible
only from inside the current package.private
members, and it cannot see P’s internal
members if P is not the current package. On the other hand, it can see the current package’s internal
members.The following example indicates adding methods to the system class String, using a newly created namespace StringExtension
:
namespace StringExtension; StringExtension extend(String) function scramble():String {...} StringExtension extend(String) function unscramble():String {...} use namespace(StringExtension); var x:String = "abc".scramble();
Once the class extension is evaluated, methods scramble
and unscramble
become available on all
strings in code within the scope of a use namespace(StringExtension)
. There is no possibility of name clashes
with extensions of class String
in other, unrelated packages because the names scramble
and unscramble
only acquire their special meanings when qualified by the namespace StringExtension
.
Unless one desires to export a class extension to other packages, the default namespace internal
generally
works best and simplifies the above example to:
extend(String) { function scramble():String {...} function unscramble():String {...} } var x:String = "abc".scramble();
Interfaces were considered for ECMAScript 4 but were dropped to keep the language simple — they are not needed as much in a dynamically typed language than in a statically typed one.
Interfaces might be defined by adding the syntax and semantics below.
Interfaces behave much like classes except that an interface I is not a supertype of a class C that
implements I. Instead, an instance c of C may be implicitly
coerced to type I, which creates an instance i of I. Implicitly
coercing i to type C yields the original instance c. It is unspecified whether c ==
i.
An interface may have both concrete and abstract members, but it may not have constructors.
In the absence of name conflicts, an interface I’s members may be accessed as properties of any instance
c of a class C that implements I. However, it is legal to define an interface I
with a member m with the same name as a member of class C and yet have the two members be different.
It is also legal for a class to implement two interfaces I and J both of which have a member named m
and have the two m’s remain distinct. Which one gets extracted when one performs the property lookup operation
c.
m depends on whether c was last coerced to one of the interfaces or to an
object type.
The Inheritance clause of class definitions would be modified to accommodate
interfaces, as listed in an implements
clause:
The newly defined class inherits instance and static members from the superclass and superinterfaces, if any. In the case of a conflict between a superclass and a superinterface, the superclass prevails. In the case of a conflict between two superinterfaces, neither is preferred and the lookup has to be qualified by the name of the superinterface for instance members or done directly on one of the superinterfaces for static members.
primitive
A primitive
class modifier attribute was considered.
This attribute would exclude null
from the set of values that can be stored in variables typed with this class:
if a class C is defined using the primitive
attribute, then null
is not considered to
be a member of the type C (there would have to be a way to specify a default value for uninitialized variables
of type C). This attribute would permit user-defined classes to behave like some predefined classes such as Number
.
This attribute was dropped for now because of circularity problems with classes that haven’t been defined yet. If
class C hasn’t been defined yet, one can still create a variable of type C; such a variable is
initialized to null
. If C turned out to be a primitive class then the variable’s value would
need to be retroactively changed.
The import
directive can be extended with more options for better
identifier and namespace management, as described below:
A package P can reference another package Q via an import
directive:
If provided, ParenListExpression should be a list
of namespaces provided by the package. These namespaces are use
d by the import
statement. In order
to resolve name conflicts between packages, IncludesExcludes provides
finer-grain control over which names are imported. include
or exclude
clauses specify which sets
of names are shared as top-level variables. If include
is used, only the listed names are made accessible; if
exclude
is used, all names except the listed ones are made accessible. For example:
package My.P1 {
explicit namespace N;
N const a = "global a";
N const b = "global b";
N class C {
static var x = 2;
}
N const c = new C(i:5); //
Initializes c.i
to 5
const x = "global x";
}
package My.P2 {
import P = My.P1, namespace(N), exclude(N::b, x); //
Imports My.P1
and use
s namespace N
, excluding N::b
and x
c; //
OK; evaluates to the instance of class C
N; //
Error: N
not found because it’s explicit
P.N; //
OK; evaluates to namespace N
in package My.P1
a; //
OK; evaluates to "global a"
b; //
Error: N::b
not found
because it’s excluded
P.b; //
OK; evaluates to "global b"
(P.N)::b; //
Error: N::b
not found
because it’s excluded
x; //
Error:
the global x
not found because it’s excluded
C.x; //
OK; evaluates to 2
}
If no include
or exclude
clause is listed, the effect is the same as if exclude()
were listed.
An import
directive does the following:
const
-bind it to
P in the current scope.explicit
top-level definition N::
n (n in namespace
N) in P, if N::
n is excluded by the given IncludesExcludes,
then skip that definition; otherwise, bind an alias N::
n to P’s N::
n
in the global scope unless N::
n is already defined in the global scope..
. Each such
expression E should evaluate to a namespace S. Evaluate use namespace(
S)
using the given IncludesExcludes.If package P has a public
top-level definition n and package Q imports P
using import PkgP =
P, then package Q can refer to n as either
n or PkgP.
n. The shorter form n is not available if it conflicts with some other
n. To avoid polluting its top-level scope, package Q can import package P using either import PkgP =
P, include()
or import PkgP =
P, exclude(
n)
, in which
case package Q can refer to n only as PkgP.
n.
If package P has an explicit
top-level definition n and package Q imports
P, then package Q can refer to that n only as PkgP.
n.
If package P has a top-level definition n in namespace N and package Q imports
P using import PkgP =
P, then package Q can refer to n
as either PkgP.
N::
n or N::
n (in either
of these the name N has to be accessible as well, which may require qualifying it if the accessibility of N
is not public
or using (PkgP.
N)
instead of N if the accessibility
of N is explicit
). Package Q can instead import P using import PkgP =
P, namespace(
N)
to be able to refer to n as plain n, barring name collisions. Alternatively, package Q can
execute import PkgP =
P followed by use namespace(
N)
(or use namespace(PkgP.
N)
) to achieve the same effect.
Earlier drafts of this proposal had a wrap pragma that caused implicit coercions of
an out-of-range integer to an integral machine type to wrap around instead of
generating an error. The pragma would only affect the portions of the program in which the pragma was lexically in effect.
The basic effect of this pragma would be to make arithmetic on sbyte
, byte
, short
,
ushort
, int
, uint
, long
, and ulong
wrap around instead of
generating errors when the result doesn’t fit in the destination.
Although useful for performance-critical code, this pragma was dropped to keep the language simple. Wrap-around can still be achieved using an explicit cast to one of the integral machine types.
Waldemar Horwat Last modified Wednesday, June 4, 2003 |