-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 This proposal, Harmonious Classes, is intended to provide a class system that is consistent with the semantics of current class mechanism in EcmaScript 3 that is based on constructors with prototypes for providing inheritance, with the intent of having semantics that are harmonious with the class construction used by the majority of EcmaScript programmers and libraries. This class system is relies on the reification of all class information, and is more similar to the class systems of Smalltalk and Lisp/CLOS. This proposal focuses on the structure and interface aspect of classes and also brings integration with typing, eliminating the need for type annotations in code (although they must still be useful/beneficial):
First, we begin with the design goals: 1. Make it possible to define the type of properties, constraining the value that can be put in a property to a certain type. Classes can define the structure of the data their instances contain. 2. All or most typing semantics and class features (there are a few exceptions) should be available without new syntax. New syntax should come, but not prior to not-syntactical methods for creating classes with type constraints. Future syntax for creating classes with typed properties and methods could be purely sugar for the existing semantics in this proposal. It is important to note this provides a means for EcmaScript programs to utilize the constraints enforced with typing, but still able to run on older systems (simply lacking the constraints). a. Add meaning and useful class and typing to ES with minimal complexity. b. Classes can be created using runtime available information (like structure information provided by the server) c. Classes can be created in JavaScript libraries (using runtime provided parameters) 3. Maximum compatibility with existing class-like object instantiation structures in ES3 a. Existing type checking mechanism work with new class structures; i.e. no new "instanceof" operators, instanceof must work with new classes. If SomeClass is a new harmonious class, then: (new SomeClass()) instanceof SomeClass -> true // constructor classes should be able to extend SomeClass function F(){} F.prototype = new SomeClass(); (new F()) instanceof SomeClass -> true // SomeClass can extend a constructor class if(SomeClass.prototype instanceof G){ (new SomeClass()) instanceof G -> true } b. When obj.method() is called, if the call succeeds, |this| should be equal to obj when method() is executed. c. Must be able to distinguish instance data from class provided defaults/methods. hasOwnProperty should distinguish instance properties from class-defined methods and defaults: (new MyClass()).hasOwnProperty("methodDefinedByClass") -> false d. Multiple inheritance - Existing multiple inheritance techniques utilize mixins + prototype delegation, more native support would be a great improvement (although this could wait to a later version) 4. Interactive Programming must be preserved. JavaScript is an interactive programming capable language, in particular existing constructor classes can have live updates by modifying the prototype object with different functions that all instances will delegate to. It is essential that we are still able to make updates without restarting the VM. 5. Multiple Inheritance - Currently mixins are used with prototype-based delegation to achieve multiple inheritance. Any new class system should be able to handle MI in some form, or at least allow for it (even if just via mixins) in order to truly be an upgrade from existing approaches. 6. Ability to call "super" methods. Clearly this is valuable since most JavaScript libraries that provide a class system attempts to provide super calls, usually even at the expense of miserable efficiency. Other future goals of classes of lower priority: 1. Private instance variables 2. Multi-method dispatch Class Instantiation New classes can be generated by instantiating the meta-class constructor. For this proposal the meta-class constructor will be considered to be stored in the global property "Class" (although in reality this would probably be a significant conflict, and would need to be a property of Object or placed somewhere else).When the meta-class constructor is called, the first argument should be an object and all it's properties will be copied to the new class. The "prototype", "methods", and "properties" properties of classes will be set to read-only and permanent as soon as the class is created. This (in combination with immutable [[Prototype]]) will ensure that all instances generated from a class will be true for a future instanceof checks against the class. When a prototype object is included all the properties of the prototype object which are functions are converted to "methods" which means that execution is guarded with type checks described below (the original function is not modified, a new method object is created to wrap it). Object Instantiation for Harmonious Classes When a new operator is used to call a class, the new object will have it's [[Prototype]] set to the prototype property of the class (exactly the same as the new operator on a function). Next, if an argument is provided to the construction of the object, all the properties of the object are copied to the new object. Finally, the "new" property of the class will be called, if it exists. (This second step could alternately be done a default (Class.prototype.new) constructor and overriden, but this would mean the class could start in a state that is not valid by the type constraints). Property Typing The type constraints for object properties are defined in the properties property of the class. Each of the values of the properties of the properties object is a schema defining the acceptable values for the instance property of the same name. A schema can be a class itself, a function, or an object describing the type. If the schema is a class or function, than the instance property value is required to be an instance of (instanceof) the class or function. If the schema is a primitive constructor, than the instance property must be a the corresponding primitive (String means the value must be a string). A schema object can have the following properties (other properties are allowed by not defined): * type - A string indicating a simple type or a union. The following are acceptable simple types (as strings): string, number, integer, boolean, object (no class specified), array, null Union type definition - An array with two or more items which indicates a union of type definitions. Each item in the array may be a simple type definition or a schema. The instance value is valid if it is of the same type as one the type definitions in the array or if it is valid by one of the schemas in the array. For example to indicate that a string or number is a valid: {"type":["string","number"]} * optional - This indicates that the instance property in the instance object is optional. This is false by default. Thus, one can then create a class: SomeClass = new Class({ properties:{ foo:String, bar:AnotherClass, baz:{type:"integer", optional: true} } } With this class, all instances will be required to have a foo property that is a string and a bar property that is also an instance of SomeClass (such that bar's value instanceof SomeClass -> true), and an optional baz property that, if present, must be a number. We could create a new instance: someObject = new SomeClass({foo:"a string", bar: anotherObject, baz: 33}); There are a number of constraints and schema definitions that could be added like defining the type for items in an array. Method Typing Classes can define the interface for methods using a similar structure as the properties typing. A class can have a methods object where each property's value is a method definition object defining the method of the same name. A method definition object consists a parameters array which is an array of schemas corresponding to the argument positions of the method. The method definition also may have a "returns" property defining the acceptable return value using a schema as well. For example: SomeClass = new Class({ // define the structure of the properties of the instance properties:{ foo:String, bar:SomeClass }, // define the method signatures methods:{ getFoo:{ parameters:[], returns:String }, setBar:{ parameters:[SomeClass] } }, // and here is the actual implementation of the class prototype:{ getFoo: function(){ return this.foo; }, setBar: function(bar){ this.bar = bar; } } } Whenever a method is called, the following type checks will occur: 1. |this| will be checked to ensure that it is an instance of the class for which it was defined. If this instanceof SomeClass is false, a TypeError will be thrown 2. The parameters will be checked to ensure that they match the constraints defined by the method definition's parameters for the class. 3. The method would then be executed. After execution the return value would be checked against the constraint (if it exists) for the "returns" property on the method definition. Class Inheritance A class may also define an "extends" property that indicates the superclass of the class. The class's "prototype" object will have it's [[Prototype]] property set to the superclass's "prototype" object. That is: SomeClass.extends.prototype == SomeClass.prototype[[Prototype]] Methods available on the superclass will be available to the instances of this class via the semantics of prototype delegation. Instances of a class must conform to the schema of the class as well as the schema of the superclass (which in turn requires conformance all the way up the chain). Consequently property and methods definitions are inherited by subclasses. super calls This particular feature would be expected to use new syntax, partly because the language has reserved a keyword for that purpose. A method can call a method from a super class with the current |this| by calling: super.methodName(parameters); When a super method is called inside a method, the method's class (this is stored as a part of method) will be retrieved. The class's superclass's prototype will then be searched for the method corresponding to the name supplied after super. and it will be executed with the current |this|. If a super method is called in a unbound function, an error should be thrown. Mutatable Classes/Interactive Programming Existing ES allows prototype objects to be mutated to change the properties and functions inherited by instances in real time. This basic functionality must be preserved in future class systems. This reifed class scheme provides the opportunity for very intuitive redefinition of classes structural definitions as well. The properties that define the structure of a class could be modified to modify the structure of class instances. However, the behavior of live structural changes to instances are certainly more complicated, with property type changes requiring coercion for instance property values. Consequently, I believe that mutable classes should remain optional for implementations. Of course most language features are useless if they are optional and can't be relied on, but mutable classes are not needed for static code, the purpose of this feature is to facilitate interactive programming and not all environments need to support this development-time feature. Some implementations may not need to incur this complexity, and could freeze the class's properties and methods objects and their child definition objects. On the other hand some implementations require this feature, such as a persistent object environment like Persevere (see below). With a server that utilizes object persistence, requiring a restart to make a structural change is simply unacceptable. Test Implementation This proposal also comes with a test implementation (albiet probably a little rough and inexact in it's implementation of the proposal, not all the features are there, but it is reasonable approximation). Persevere (a server side JavaScript system based on Rhino) implements this class system and can be downloaded at http://code.google.com/p/persevere-framework/ (download, unzip, and run java -jar start.jar to try it out). Persevere is designed to be an application server, but it has a console from which you can interact with the Rhino JavaScript environment with this class system. For example (it should be noted that there some unique characteristics to the Persevere runtime: the global object is frozen to prevent sharing information between threads, but the console has a local scope, so all assignments must be vars. Also all new classes and class instances are persisted): js>var Product = new Class({ properties: { price: Number, name: String }, methods: { order: { parameters:[Integer] } }, prototype: { order: function(quantity){ console.log("ok, I am ordering " + quantity); }, toString: function(){ return "I am a " + this.name; } } }); // make sure to get a newline after the last line to get the whole block on the console as one command js>Product.properties.price function Number() { [native code for Number.Number, arity=1] } js>var widget = new Product({name:"Widget", price:9.95}); js>widget.price = "high"; org.mozilla.javascript.EcmaError: TypeError: A string is not allowed, a number is required for property price js>widget.price = 12.95; 12.95 js>widget.order(); org.mozilla.javascript.EcmaError: TypeError: A value is required for parameter 1 js>widget.order(3); WARNING: ok, I am ordering 3 js>var Shoe = new Class({ "extends": Product, properties: { size: {type:"string", description:"Shoe size in US units"} }, prototype: { toString: function(){ return "I am a shoe called " + this.name; } } }); js>var shoe = new Shoe({name:"Air Ecma"}); org.mozilla.javascript.EcmaError: TypeError: A value is required for property size js>var shoe = new Shoe({name:"Air Ecma", price:99.95, size: "9.5"}); js>shoe.toString(); I am a shoe called Air Ecma // and even some "live" type changes: js>typeof shoe.size string js>Shoe.properties.size = Number; js>shoe.size 9.5 js>typeof shoe.size number (The following features would be less critical:) Records The main use case of records (types defined by structure), is for asserting the properties of object passed as parameters to methods. It may make sense to allow for object's that follow the structure of classes (with a properties object) to define structures that are allowed as a parameters. It is worth noting that is much more of a form of an assertion than actual typing. Multiple Inheritance Classes can inherit from multiple superclasses by using providing an array of classes for the "extends" property. Instances of the class must conform to the properties and method definition of all the superclasses (if there is conflicting definitions than it would be impossible to make instances of that class, throwing an error early would be nice). The [[Prototype]] of the class's "prototype" property would be set to a special object that takes the array and does multiple delegation, trying each item in the array to find a property value and return the first value that is found in the array. I could expound on this idea more, but it is not central to this proposal. Security Considerations One of the biggest security problems with constructors and classes in EcmaScript from an object-capability perspective is that a reference to class/constructor provides access to the prototype object from which modifications can be made. Often one may wish to provide a constructor for object instantiation (and possibly type checking) to another module without actually giving full access to the class object. One way to provide this functionality is to create class proxy when the "new" property/constructor is extracted. The value returned from getting the "new" property could be used to create new instances of the class, but provide no reference back to the actual class and all it's property and prototype definitions. This would allow class-like object generators to be passed to untrusted code while adhering to least-priviledge principles. It may also be valuable to _not_ add a "constructor" property to the prototype of classes by default. This prevents code from accessing the class of an object without being explicitly provided that capability. Possible future syntax This proposal focuses on semantics without new syntax, but I believe that adding private instance variables should be done by adding new syntax. The current proposals for adding this capability as sugar fail to meet a number of the design goals list above. Here is example of how I would envision class syntax, sugar for defining structure and new syntax for defining the private instance variables by creating a scope that has a unique set of values for each class instance: class(let myPrivateVariable) { // object literal syntax extends: OtherClass, properties:{ foo: String }, prototype:{ getFoo: function(){ return this.foo + myPrivateVariable; } }, "new": function(x){ myPrivateVariable = x + 3; } } Perhaps the biggest concern with the old EcmaScript 4 proposal was the fusion of a Java-like type system with JavaScript. This class system was inconsistent with the inheritance and class-like construction that already existed in the language, creating for a very confusing mix of semantics, inconsistent inheritance schemes and duplicity of instance checking operators. But at the same time, "programming in the large" can greatly benefit from the integrity provided by classes and typing. Harmonious classes provide this integrity in a way that properly builds on EcmaScript 3, using a system much more similar Smalltalk and CLOS/Lisp class systems. All classes are completely reified, all information that is used to define the structure and interface of class instances is defined and accessible with the EcmaScript 3 object model. Reasons for this system: 1. A class system that is based solely on new syntax can't be used until there is complete adoption in the browsers. This is common problem with new features, usually users can't benefit from a feature until can rely on it's presence in all your target environments. However, with type constraints, this is not true. Typing is fundamentally a development tool, providing information for correctness analysis and asserting assumptions. And just like assertions, outside of development, the tool is not absolutely necessary. With JavaScript, typing information will almost certainly always be removed by minifiers for production code. As development tool, like Firebug, the benefits are still completely available as long as there is a single implementation, even in the absence of ubiquity. 2. Clean separation of interface and implementation in classes is possible. 3. Typing is provided by the class system, type annotations are not necessary. While it is not possible to define types for local variables, typing local variables rarely adds useful information to the analysis and integrity of a program, most type information can be inferred. The unification of classes and typing greatly reduces the overall complexity of the language. 4. Interactive programming can be preserved. There are no fundamental semantics that prevent real-time programming. It would be a terrible regression if future ES versions forced developers back in to the old edit, restart (reload the page in the browser on the web) and test cycle that is so slow. I could also include a JavaScript/psuedocode implementation to provide a more detailed description of the mechanics of this class system. Thanks, Kris Kris Zyp wrote: > > > ----- Original Message ----- > *From:* Kris Zyp <mailto:[EMAIL PROTECTED]> > *To:* es4-discuss Discuss <mailto:[EMAIL PROTECTED]> ; > [EMAIL PROTECTED] <mailto:[EMAIL PROTECTED]> > *Sent:* Friday, July 25, 2008 9:01 AM > *Subject:* Typing with schemas instead of annotations > > I wanted to propose an alternate approach to defining types > within ES. I don't think is actually a realistic proposal for > changing ES4 or ES3.1, but more of as an interesting alternate > language mechanism for utilizing the ES4 VM with ES3 syntax that > I have been interested in exploring for a while now, and wanting > to write out in case anyone was interested in the idea. The > basic premise is to define Classes, records, and parameter types > with a schema object that can easily defined with ES3/JSON, > rather than using ES4 type annotation syntax. Type information > would be defined with a schema that would act like an interface, > and this could be used in combination with local type inference > for local variables. My examples and proposal are based on using > JSON schema[1] for defining types; my involvement in JSON Schema > might preclude objectivity to some degree, but I do think it is > the most reasonable ES3 compatible definition for types. > Expressed in ES3, it is a simple object structure, with a high > degree of correspondence to ES4 types. However, though the > proposal is more about using a external type definition/schema > even if an alternate format (like Cerny [2]) might be better. > > This approach could have application even without actual native > support. Development could potentially use this approach to have > a codebase that could easily be adapted (possibly through > automated transformation, or type omission) to various target ES > VMs. Also this approach has nothing to do with "classes as > sugar" proposal [3]; it could actually be considered the > inverse. Rather than attempting ES4 class syntax with ES3(.1) > semantics, it is about using ES3 syntax to drive ES4 > class/record semantics. This proposal is also not a complete in > it's ability to define ES4 semantics with ES3 syntax. There are > definitely plenty of typing forms that this approach can't > define, but I would suggest that the majority of type > definitions can be defined or inferred with this approach. > > Motivation > > 1. The first motivation is that the code could run in ES3 or ES4 > VMs. Of course the ES3 VM doesn't have native typing support, so > it would either have to go without typing, or do code translation. > > 2. Separation of behavior and typing/structure concerns. > a. One can look at the schema for the strucuture and > interface of the a given Class or data structure separately from > looking at the code for a nice clean, minimal (no annotations) > look at the implementation and behavior. > b. One can easily include or exclude typing information for > code. Certainly the biggest benefits of the ES4 type system are > at development time, with the integrity, correctness, > organization, and code completion assistance. Once in > production, the majority of web applications spend far more time > downloading JavaScript than they do executing it (especially > after DOM deductions). Applications may be becoming more > sophisticated, but sophisticated apps still increase download > time and with ES VMs quickly improving, plus hardware > improvements increasing at a faster rate than bandwidth widens, > I think download times will continue to dominant execution times > for quite a while. Consequently, it seems most likely that > performance-savvy developers will mostly like strip the majority > of type annotations off of code during compression (perhaps > retaining some annotations for performance sensitive hot-spots > if VMs prove to benefit from typing information, or retaining it > in situations where correct execution depends on type errors, > rather than only signalling incorrect execution). > c. Existing applications could be retrofitted with type > information without modifying the original code (or minimally > modifying). Since type information is provided through a > separate schema, the original code can be kept intact. > > 3. Class reflection has an obvious reification based on the > schema. Using JSON Schema, type information is reflected as the > schema object itself. > > 4. Language neutral interfaces - JSON Schema has been designed > to be a language agnostic (I realize that might be a little > wistful, JSON Schema bears influence from the primitives of JSON > and ES, but JSON has proven pretty cross-language capable). One > could actually use a JSON Schema defined structure or interface > with implementations in various languages. This can even > facilitate interaction through non-language-specific means (like > RPCs), and JSON Schema is even being used for such with Simple > Method Description with Dojo [4]. > > Example: > > First we define the schema interface for our WebMail class, this > could actually be in a separate file than the class impl, (note > that we are doing it in pure JSON, except for a few comments, > lot's of sugar could easily be applied): > Schema.WebMail = { > description:"Webmail Client", > type:"object", > methods:{ // these are the methods for the Webmail class > send: {parameters:[{$ref:"Msg"}]}, // refers to the Msg > type defined below > handleMessage: {returns:{$ref:"Msg"}}, > fetch: {returns:{$ref:"Msg"}}, > get: {parameters:[{type:"integer",name:"id"}], > returns:{$ref:"Msg"}} > }, > properties:{ > database:{ > type:"array", > items:{ > id:"Msg", > description: "Email message", > properties:{ > to:{ > type:"array", > items:{$ref:"Addr"} // use the Addr type > }, > from:{ > id:"Addr", > description: "Email address", > properties:{ > at:[{type:"string"},{type:"string"}], > name:{type:"string"} > } > }, > subject:{type:"string"}, > body:{type:"string"}, > read:{type:"boolean"}, > messageId:{type:"integer",unique:true,optional:true} > } > } > } > } > }; > > Now we define the actual class impl, once again this could be in > a separate file than the schema: > > WebMail = Class(Schema.Webmail,{ > send:function (msg) { // msg is determined to be a Msg from > the schema > msg.messageId = sendToServer(JSON.encode(msg)); > // this is typed check, msg and this.database[n] are both > known to be Msgs from the schema > this.database[msg.messageId] = msg; > }, > get: function(n) { > if (n in this.database) > // The value of database[n] can be determined from the > schema, > // and it properly matches the return type of get > return this.database[n]; > // handleMessage also has a return type that matches get, > so it is safe > return this.handleMessage(n); > }, > showMessage: function(n){ > //local type inference can determine the type of msg from > the return type of get from the schema: > var msg = this.get(n); > msg.read = 40; // this could statically be determined to > be a type conversion error > msg.read = true; // this is a correct assignment > messagePane.innerHTML = msg.body; // can statically be > determined to be a string (conversion check not necessary) > }, > handleMessage: function(n) { > // this line is tricky, should we auto cast like "wrap"? > var msg = JSON.parse(fetchFromServer(n)); > if (msg.result == "no data") > return null; > // if we auto-wrap, the casting can be done on this > // line or the first line depending on local type inferencing > return database[msg.messageId] = msg; > }, > database:[] > } > ); > > This approach creates the class implementation explicitly using > the schema, with the schema reference being in the class > constructor. One could also use a mechanism where the class > loader would use a name-based convention for loading the schemas > and connecting them to class constructors. This could be used to > facilitate applying schemas/typing to completely pure unaltered > ES3 code/files. > > This is example is taken from a comparison of ES4 and ES3 (with > typing checks) code size [3]. This approach provides an > implementation that is more compact than either. Of course, this > could be seen as just moving the extra text to another place, > the schema, but this is intentional. The schema acts more as > documentation, a contract for implementations, generous with > descriptions and various other informative (and probably > non-normative to the type checker) annotations. Implementations > can benefit from minimal code side, and documentation/contract > information can be extensively detailed and informative. > > Note that it is also important that the schema and > implementation be static/declarative (like JSON, no dynamic > expressions) in order to be properly statically analyzed. It > would be quite easy to create aliases/consts for parts of a > schema if you didn't expect it to be shared with other > languages/systems. > > There would certainly be some interesting issues in exactly how > to map a schema to ES4 semantics. I think it would make sense > for method-less schemas to generally map to structural types > instead of interfaces. Converting plain objects (from functions > that return untyped objects) to typed variables and slots would > probably require creating a new object with original properties > mixed in (like the old wrap proxy). I am sure there would be > other issues to solve, and some ES4 type semantics that simply > couldn't be described. > > Anyway, once again this is not so much a proposal for changing > ES, but an exploration of an alternate approach to typing and > defining type constraints for various ES VMs. Perhaps this > discussion might be better done on LtU, not sure... > > [1] http://groups.google.com/group/json-schema / > http://groups.google.com/group/json-schema/web/json-schema-proposal---second-draft > [2] http://www.cerny-online.com/cerny.js/documentation/guides/schema > [3] > https://mail.mozilla.org/pipermail/es4-discuss/2008-March/002496.html > [4] > http://www.sitepen.com/blog/2008/03/19/pluggable-web-services-with-smd/ / > http://groups.google.com/group/json-schema/web/service-mapping-description-proposal > > Thanks, > Kris > - -- Kris Zyp SitePen (503) 806-1841 http://sitepen.com -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.9 (MingW32) Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org iEYEARECAAYFAkktVN4ACgkQ9VpNnHc4zAxPnACeKDnVC7nj9iURQMzGAfcnxVEt 8psAoLhGDH2DodrsKRe0yc8vo1mZ1q1g =+30P -----END PGP SIGNATURE----- _______________________________________________ Es-discuss mailing list Es-discuss@mozilla.org https://mail.mozilla.org/listinfo/es-discuss