ECMAScript 4 Netscape Proposal
Rationale
Miscellaneous
previousupnext

Wednesday, June 4, 2003

This section presents a number of miscellaneous alternatives that were considered while developing this proposal.

Types

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.

Typed Arrays

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:

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.

Type Expressions

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.

Multiple Constructors

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.

Calling a Superconstructor

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:

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.

Class Extensions

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 Attribute

The 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:

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

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.

Interface Definition

InterfaceDefinition 
   interface Identifier ExtendsList Block
|  interface Identifier Semicolon
ExtendsList 
   «empty»
|  extends TypeExpressionList
TypeExpressionList 
   TypeExpressionallowIn
|  TypeExpressionList , TypeExpressionallowIn

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.

Class Definitions

The Inheritance clause of class definitions would be modified to accommodate interfaces, as listed in an implements clause:

ClassDefinition  class Identifier Inheritance Block
Inheritance 
   «empty»
|  extends TypeExpressionallowIn
|  implements TypeExpressionList
|  extends TypeExpressionallowIn implements TypeExpressionList

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.

Attributes

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.

Importing Packages

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:

ImportDirective 
   import ImportBinding IncludesExcludes
|  import ImportBinding , namespace ParenListExpression IncludesExcludes
ImportBinding 
   PackageName
|  Identifier = PackageName
IncludesExcludes 
   «empty»
|  , exclude ( NamePatterns )
|  , include ( NamePatterns )
NamePatterns 
   «empty»
|  NamePatternList
NamePatternList 
   QualifiedIdentifier
|  NamePatternList , QualifiedIdentifier

If provided, ParenListExpression should be a list of namespaces provided by the package. These namespaces are used 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 uses 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:

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.

Wrap Mode

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
previousupnext