strawman: Harmonious Classes and Typing

Kris Zyp kris at sitepen.com
Wed Nov 26 05:53:35 PST 2008


-----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:kris at sitepen.com>
>     *To:* es4-discuss Discuss <mailto:es4-discuss at mozilla.org> ;
>     es3.x-discuss at mozilla.org <mailto:es3.x-discuss at mozilla.org>
>     *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-----



More information about the Es-discuss mailing list