How much sugar do classes need?
2008/11/22 Mark S. Miller <erights at google.com>:
// before desugaring class Point(x, y) {
Having arguments makes this entire example seem like a function. That is ok, perhaps good even, but some of the code inside the class block does not seem like it will execute as though the class is a function call or even close. The static code seems out of place.
static {
It isn't clear that static members is a must. But if they are...
let privClassVar = 1; const privClassConst = -1;
So members are private by default? I like that and it is consistent with a style of JavaScript OOP popular today. In the wiki page members are public by default which is as dangerous as implied globals (i.e. a missing "var") in ES3.
Point.pubClassConst = -3;
Why not the following?
public pubClassConst = -3
}
In Java a static block runs when the class is first loaded. Is that the case here? I think the ability to initialize the class is important if some setup needs to be done to establish the class members.
For declaring class variables, I prefer the static modifier in the wiki page
static let privClassVar = 1;
let privInstVar = 2; const privInstConst = -2;
Does "const" have "let" scoping?
public self implements Point { to toString() { return '<' + self.getX() + ',' + self.getY() + '>'; }, to getX() { return x; }, to getY() { return y; }
What do the "to" modifiers do?
let pubInstVar: 4, const pubInstConst: -4 };
I'm not really a fan of this section, in general. I prefer the syntax similar to the wiki page
public function toString() { return '<' + self.getX() + ',' + self.getY() + '>'; }; public const pubInstConst = -4;
return self;
Why the explicit need to return "self"? That doesn't seem very "sweet". It seems like boilerplate.
}
The wiki page shows a "private" keyword. It seems to me, there is no need for a "private" keyword at all to get instance private. I think instance private is the good kind of private. As shown in your example, the following can be instance private.
class Point(x, y) { var instPrivVar = 1; }
If class private is desired, which I don't think it is, then I think some other modifier should be introduced (i.e. not "private" as "private" should me totally "private".)
There is no inheritance here and so Java's "protected" doesn't seem to be an issue.
Leaving class members out of the picture makes the whole issue much clearer. I was thinking and kind of had the impression we might be headed to writing something like the following
class Point(x1, y1) {
var privInstVar = 2; const privInstConst = -2;
// code that needs to run at construction time // is interspersed anywhere inside the class block var foo = 3 * x + y;
function privFn(a) foo + a;
public function toString() { return '<' + x1 + ',' + y1 + '>'; }; public get x = function() x1; public get y = function() y1; public var pubInstVar = 4; public const pubInstConst = -4;
}
The above would desugar to the same style of how some programmers are writing JavaScript code today. I write code roughly like that and would write code just like it if I could. I think many JavaScript programmers would take only a few minutes to learn how to write code like my example above...and many would breath a big sigh of relief.
Added benefits of the sugar are auto-freeze (allowing type checking as you noted) and compiler/interpreter optimizations.
I used "var" instead of "let" in my example. I don't know any advantage of "let" here.
If there are ever going to be multi-methods (perhaps dispatching on type or other properties), then the examples we have both shown seem inappropriate as there can only be one constructor. In this case it might be beneficial/necessary to go more to the Java-style
class Point {
var privInstVar = 2; const privInstConst = -2; var x1, y1; var foo;
function Point(x:int, y:int) { // no "public" in constructor as slots allocated // at compile time and different constructors // shouldn't add different public properties x1 = x; y1 = y; // construction-time code must be in a constructor foo = 3 * x + y; }
function privFn(a) foo + a;
public function toString() { return '<' + x1 + ',' + y1 + '>'; }; public get x = function() x1; public get y = function() y1; public var pubInstVar = 4; public const pubInstConst = -4;
}
That is pretty darn Java-ish but the class syntax part of Java is not the problem with that language. The other parts are. ;-)
Another thing I noticed in the wiki page is a possible syntax for members
public x; public bar() {}
This makes "public <identifier>" short for "public var <identifier>"
and, more importantly, makes "public <identifier>()" short for "public function <identifier>()"
If that is going to exist, and that means "function", not "lambda" (if lambda makes it into the language), then why not the following for a function definition?
var bar() {}
If "var bar() {}" is not allowed then I don't see why "public bar() {}" would be allowed. I don't think class sugar should make the language more asymmetrical as asymmetry makes a language difficult to learn and understand. Symmetry makes a consistent, predictable language. Symmetry equals success in nature for a reason.
Peter
On Sat, Nov 22, 2008 at 8:54 PM, Peter Michaux <petermichaux at gmail.com>wrote:
2008/11/22 Mark S. Miller <erights at google.com>:
Point.pubClassConst = -3;
Why not the following?
public pubClassConst = -3
I'm glad you raised this first. This example is a good microcosm of the issues distinguishing the two sugar proposal. My reasons for preferring the first:
-
The second form desugars to the first, but is no more convenient than the first. So introducing sugar here is not justified on convenience grounds.
-
The second form obscures the difference between variables and properties, which does the ES programmer no favors. In ES, "instance private" means lexically captured variable. ES has no other encapsulation mechanism, and the essence of the classes-as-sugar proposal is to employ that mechanism to create encapsulated instances. "Public" means exactly string-named property. (A third category, class-private instance variables, depends on the introduction of Name objects, which both Cormac and I have left out of our first examples.)
As for "familiarity to Java programmers", more people learn ES as their first language that Java. Java programmers have already learned to program. Making things easier for Java programmers to learn, at the price of making it harder for new programmers and for ES3 programmers (as well as functional programmers) seems like a bad tradeoff. ES already won where Java lost. There's no reason for us to imitate its mistakes.
It probably makes sense to argue through this one example first, as most of the rest of the differences between the sugar proposals have these same motivations.
On Sun, Nov 23, 2008 at 7:24 AM, Mark S. Miller <erights at google.com> wrote:
On Sat, Nov 22, 2008 at 8:54 PM, Peter Michaux <petermichaux at gmail.com> wrote:
2008/11/22 Mark S. Miller <erights at google.com>:
Point.pubClassConst = -3;
Why not the following?
public pubClassConst = -3
I'm glad you raised this first. This example is a good microcosm of the issues distinguishing the two sugar proposal. My reasons for preferring the first:
- The second form desugars to the first, but is no more convenient than the first. So introducing sugar here is not justified on convenience grounds.
At the very least, using the name of a class inside the class is a problem for refactoring. If the code is moved to another class or the class is renamed then all the names need to be changed.
If classes were to inherit class methods from parent classes (not implying single inheritance) then your syntax would/may make it seem that the property must be accessed through the Public object and couldn't be accessed through a subclass object.
- The second form obscures the difference between variables and properties, which does the ES programmer no favors.
I don't agree that it would be confusing to the ES programmer.
In ES, "instance private" means lexically captured variable. ES has no other encapsulation mechanism, and the essence of the classes-as-sugar proposal is to employ that mechanism to create encapsulated instances.
Indeed. I thought that hiding that mechanism is part of the sugar's objective.
"Public" means exactly string-named property. (A third category, class-private instance variables, depends on the introduction of Name objects, which both Cormac and I have left out of our first examples.)
I understand your point. I don't think this sugar is the sugar for which people are hoping. If making a public method means explicitly adding a property to an object then your proposed sugar is only nutrasweet ;-) I mean that jokingly. I understand you are trying to make a minimal proposal to see how minimal it can be.
I think part of the appeal of writing the following is it is declarative.
public pubClassConst = -3
Writing the following is imperative.
Public.pubClassConst = -3
I think your idea of adding properties to mean "public" is also the reason for the "return self" in your example which I saw and didn't understand. Why did you use "self" and not "this" which is a constructor's automatic return value? If "look ma, no "this"' is possible only because "self" is used in it's place then that seems quite a superficial claim.
Why did you use the "public" keyword at all in the following line
public self implements Point {
For consistency with this properties idea, should/could that section be
var self = { to toString() { return '<' + self.getX() + ',' + self.getY() + '>'; }, to getX() { return x; }, to getY() { return y; } let pubInstVar: 4, const pubInstConst: -4 }; return self;
I've removed the implements because that is related to type checking and not this properties vs "public" issue. I'm guessing the "to" is a typo and there is a missing comma. I don't understand the need for the "let".
If there is to be inheritance (not necessarily single inheritance), and something like Java's "protected" modifier for subclass-only access, then how would that fit into your system?
As for "familiarity to Java programmers", more people learn ES as their first language that Java.
Is that true?
Java programmers have already learned to program. Making things easier for Java programmers to learn, at the price of making it harder for new programmers and for ES3 programmers (as well as functional programmers) seems like a bad tradeoff.
I think functional programmers would be more interested in having multi-methods. Moving to an exclusive multi-method system is not really possible given the entire DOM API is message passing.
ES already won where Java lost. There's no reason for us to imitate its mistakes.
I wasn't advocating anything because it is familiar to Java programmers. If ever I do mention doing something like Java does, it will be both because Java does a few things well and it will be rare. When talking about classes, Java is bound to be discussed.
It probably makes sense to argue through this one example first, as most of the rest of the differences between the sugar proposals have these same motivations.
Peter
2008/11/22 Mark S. Miller <erights at google.com>:
const Point = let { // after desugaring let privClassVar = 1; const privClassConst = -1; Point.pubClassConst = -3;
function Point(x, y) { let privInstVar = 2; const privInstConst = -2; const self = Object.create(Point.prototype); Object.defineProperties(self, { toString: {value: Object.freeze(function() { return '<' + self.getX() + ',' + self.getY() + '>'; })}, getX: {value: Object.freeze(function() { return x; })}, getY: {value: Object.freeze(function() { return y; })}, pubInstVar: {value 4, writable: true, enumerable: true}, pubInstConst: {value: -4, enumerable: true}, }); Object.preventExtensions(self); return self; } Object.freeze(Point);
};
I jumped straight to looking at your proposed sugar and that lead me to some confusion because the above unsugared code is not the pattern I think the sugar should be coating. I think it is good to look at both the sugared and desugared versions at the same time.
With the above code and in the sugared code, an instance methods accesses a public instance member like "self.getX()" in toString. This is verbose and this brings up the issue of whether or not the idea of properties and variables should be blurred. In this case of classes, I think the goal is to blur them so that access to public members is non-verbose from anywhere inside the class. As an example, I'll rewrite the above desugared code so that access is easy. I'm leaving out class properties. I think the following is the pattern the sugar should be coating.
const makePoint = function(privX, privY) {
const self = Object.create(Point.prototype);
let privInstVar = 2; const privInstConst = -2;
function toString() { // no need for "self." return '<' + x() + ',' + y() + '>'; } Object.defineProperties( self, "toString" {value: Object.freeze(function() {return toString();})});
function x() { return privX; } Object.defineGetter( self, "x" {value: Object.freeze(function() {return x();})});
function y() { return privY; } Object.defineGetter( self, "y" {value: Object.freeze(function() {return y();})});
var pubInstVar = 4; // not sure how to write the next section properly with // the necessary attributes. Object.defineGetter( self, "pubInstVar", {value: Object.freeze(function() {return pubInstVar;}), writable: true, enumerable: true}); Object.defineSetter( self, "pubInstVar", {value: Object.freeze(function(val) {pubInstVar = val;}), writable: true, enumerable: true});
var pubInstConst = -4; Object.defineGetter( self, "pubInstConst", {value: Object.freeze(function() {return pubInstConst;}), enumerable: true});
Object.preventExtensions(self); return self; };
With the above code, the "public" keyword sugar has obviously greater purpose. All of the mentions to "self" disappear. The aggregation of that object's properties is almost automatic. The use of "private" does the work.
class Point(privX, privY) { let privInstVar = 2; const privInstConst = -2;
public function toString() { return '<' + x() + ',' + y() + '>'; }
public get function x() { return privX; }
public get function y() { return privY; }
public var pubInstVar = 4;
public const pubInstConst = -4; };
All the interleaving code that establishes "self" and adds properties is gone.
One way to interpret my desugared and sugared versions is that all members can be accessed as though they are private. Public members should be accessible uniformly the way that private members are accessed. An instance has permission to access all of its members and shouldn't need to work hard to access the public members. The use of "public" is aggregating some of the private members that should also be made available publicly.
With the sugar in the wiki page for public function members and function expressions, things become extremely compact
class Point(privX, privY) { let privInstVar = 2; const privInstConst = -2; public toString() '<' + x() + ',' + y() + '>'; public get x() privX; public get y() privY; public pubInstVar = 4; public const pubInstConst = -4; };
And I think that is looking quite appealing.
Peter
On 2008-11-22, at 23:54EST, Peter Michaux wrote:
let privClassVar = 1; const privClassConst = -1;
So members are private by default? I like that and it is consistent with a style of JavaScript OOP popular today. In the wiki page members are public by default which is as dangerous as implied globals (i.e. a missing "var") in ES3.
My 2p: I feel just the opposite. Conflating classes (OOP) with
access control is a big mistake in my book, a big mistake made by Java
that we ought to learn from. It leads you to either write really huge
classes, or to have to 'decorate' all your classes with public
declarations. Access control should have its own orthogonal mechanism
-- modules or packages that can consist of one or more classes and can
gather the access control declarations in one spot so they are easily
analyzed, as opposed to scattering them over all the component classes.
On Mon, Nov 24, 2008 at 7:37 AM, P T Withington <ptw at pobox.com> wrote:
On 2008-11-22, at 23:54EST, Peter Michaux wrote:
let privClassVar = 1; const privClassConst = -1;
So members are private by default? I like that and it is consistent with a style of JavaScript OOP popular today. In the wiki page members are public by default which is as dangerous as implied globals (i.e. a missing "var") in ES3.
My 2p: I feel just the opposite. Conflating classes (OOP) with access control is a big mistake in my book, a big mistake made by Java that we ought to learn from. It leads you to either write really huge classes, or to have to 'decorate' all your classes with public declarations.
Why do private members cause "really huge classes"?
Access control should have its own orthogonal mechanism -- modules or packages that can consist of one or more classes and can gather the access control declarations in one spot so they are easily analyzed, as opposed to scattering them over all the component classes.
If access control for a class member was not written with that class member, then it would mean a lot of hunting around the code to determine if a member is public or private. That seems like a recipe for trouble to me. It reminds me of some aspect oriented programming where hunting and gathering is necessary to determine if a filter is being applied to a particular method or not. That is a lot of hard work and something I've worked to avoid at all costs.
Peter
On Sun, Nov 23, 2008 at 10:17 AM, Peter Michaux <petermichaux at gmail.com>wrote:
In ES, "instance private" means lexically captured variable. ES has no other encapsulation mechanism, and the essence of the classes-as-sugar proposal is to employ that mechanism to create encapsulated instances.
Indeed. I thought that hiding that mechanism is part of the sugar's objective.
Clearly, different people seek different objectives, but there are some objectives on which there seems to be general agreement. However, I don't know that there's been any previous attempt to capture this fledgling consensus. Since everyone likely to disagree is on es-discuss, I'll take a shot here.
-
Integrity: An abstraction mechanism that supports and encourages higher integrity abstractions, by contrast with ES3's general loose (everything can mess with almost everything) approach. In particular, it should be possible to determine from the source code of the class, and not much else, what invariants are maintained by instances of that class.
-
Encapsulation: Clients of an instance should be able to utilize its public interface but not access its private state. (If you want to consider this point to be required by the Integrity bullet, I have no argument.)
-
Avoid accident prone constructs: For example, in the traditional ES3 style of class-like programming, methods can be extracted and applied to other objects or even invoked as constructors, binding their this to whatever. This was the motivations for the binding-on-extraction behavior of ES4 methods. (Again, feel free to consider this implied by the Integrity bullet.)
-
Minimize new kernel semantics: As demonstrated by various proposed desugarings, ES3.1 already contains adequate mechanism for the kinds of high integrity programming people expect from ES-H classes. Ideally then, classes would add no new kernel semantics at all to ES-H, but merely desugar to other elements already present. (The one exception we will probably make is more direct support for nominal types and type constrained variables and properties.)
-
(Brendan's phrase) "Syntax as user interface": When the desugared form is either a) too verbose or hard to read/write, or b) too much harder to understand than the sugared concept it represents, then some kind of convenience or abstraction, respectively, is called for. This may take the form of syntactic sugar, additional libraries, or some combination. However, in the interests of simplicity, we should minimize the introduction of sugar, and should prefer libraries to the introduction of new syntax. Hence the title of this thread.
-
Guidance for future optimizations: As language specifiers, we must constrain our specifications to those that can be implemented well. But specification by desugaring can specify a naive inefficient desugaring, so long as we are confident that an obervably equivanlent but efficient implementation is practical by other means. Ideally, classes should be easier to make efficient than the existing class-like pattern people use in ES3. This was one of the motivations for ES4's fixtures.
Let's start with Crock's original objects-as-closure pattern applied to the present example (without statics or nominal typing) and updated to use remaining elements of ES-Harmony, and then refine it according to the above objectives:
function Point(privX, privY) { let privInstVar = 2; const privInstConst = -2;
const self = { toString: function() { return '<' + self.getX() + ',' + self.getY() + '>'; }, getX: function() { return privX; }, getY: function() { return privY; }, pubInstVar: 4, pubInstConst: -4 }; return self; }
Now I can answer the specific question you ask above. Crock's use of lexically catured variables to represent private instance state is beautiful, and needs no sugar. If its semantics were adequate, I would argue that it needs no sugar at all, as none of the proposed class syntaxes, and none of the class syntaxes from other languages, are any clearer than this.
By our criteria above:
-
Integrity: Bad. A point instance is simply a mutable record. If two clients share access to a point, either can arbitrarily replace, delete, or extend any of its properties, violating any invariant any other client of the same point may be relying on. Likewise, Point, pubInstConst, and the three methods are needlessly mutable. pubInstVar is needlessly configurable.
-
Encapsulation: Good. Clients of a point instance cannot access its encapsulated state. Period.
-
Avoid accident prone constructs: Moderate. On the positive side, because self is simply a lexically captured variable, extracted methods are already bound to their instance without any new mechanism. Because they do not mention 'this', little damage arises if a function/method/constructor is invoked as a constructor/function/method, etc. OTOH, All five properties are enumerable though probably only the two data members should be.
-
Minimize new kernel semantics: Perfect.
-
Syntax as user interface: Good. I would have given this one a "Perfect" but for your point about access public members from inside as simple variable accesses. Obviously, one could instead write
function getX() { return privX; } const self = { ... getX: getX, ...
but this isn't perfect either. I'll come back to this in the next email.
- Guidance for future optimizations: Moderate. The cost of the objects-as-closures technique on current implementations is a closure allocation per method per instance. Until we address the pervasive loose mutability of everything, this extra cost would be difficult to optimize away.
The desugaring I presented at the beginning of this thread addresses all the weaknesses above, primarily by using the new ES3.1 Object methods to control the attributes of properties and the extensibility of objects. Unfortunately, in so doing, it weakened #5 considerably. The result is chock full of so much verbose boilerplate as to look more like compiler output than human written code. As far as I am concerned, that verbosity is the only reason classes need sugar.
In looking back over the original before and after, we can separate two issues:
- The need for an easier syntax for frozen functions.
- The need for a more expressive way (than the current object literal) to create an instance.
Both "class" and "to" in my earlier proposal serve to declare a frozen function. The "static" block was a way to add properties to a function before it is frozen. The "public" block was my first attempt at a more expressive object-literal-like notation. In retrospect, I probably should use the term "object" for now to avoid confusion, and will below. The "to" says to treat the following function as a method: using its name as a property name, and making this property non-wriatble, non-enumerable, and non-configurable, and with a freezing of this function as its value.
I understand your point. I don't think this sugar is the sugar for which people are hoping.
I hope this thread provokes people to explain what they hope for. That would be great.
If making a public method means explicitly adding a property to an object then your proposed sugar is only nutrasweet ;-) I mean that jokingly. I understand you are trying to make a minimal proposal to see how minimal it can be.
Indeed. A sweet syntax that doesn't cause the growth of fat! ;)
I think part of the appeal of writing the following is it is declarative.
public pubClassConst = -3
Writing the following is imperative.
Public.pubClassConst = -3
If the first desugars to the second, then it is equally imperative. You're just obscuring the imperative nature from the programmer. Since the order of side effects is one of the main sources of programming hazards, again, I don't think such cosmetic declarativeness is helpful.
I think your idea of adding properties to mean "public" is also the reason for the "return self" in your example which I saw and didn't understand. Why did you use "self" and not "this" which is a constructor's automatic return value? If "look ma, no "this"' is possible only because "self" is used in it's place then that seems quite a superficial claim.
The "return self;" is there because the "class" sugar and the "object" (aka "public") sugar are distinct mechanisms that can be used separately or together. I use "self" because ES's semantics for "this" are at odds with goals #1, #2, and #3. By explicitly naming "self", lexically nested objects can choose distinct names, giving inner objects a clean way to refer to outer objects without confusion.
Why did you use the "public" keyword at all in the following line
public self implements Point {
For consistency with this properties idea, should/could that section be
var self = { to toString() { return '<' + self.getX() + ',' + self.getY() + '>'; }, to getX() { return x; }, to getY() { return y; } let pubInstVar: 4, const pubInstConst: -4 }; return self;
I've removed the implements because that is related to type checking and not this properties vs "public" issue. I'm guessing the "to" is a typo and there is a missing comma. I don't understand the need for the "let".
You still need to preventExtensions on "self" before returning it. Hence "object" aka "public".
Yes, let's leave aside the "implements" and related type checking issues for now.
Your "var" above should be a "const", so that we'll have a read barrier preventing any use of self before it's constructed.
The "to" isn't a typo, but wasn't explained. It creates a non-writable, non-enumerable, non-configurable property whose value is a frozen function.
The "let" is probably misconceived, but was to indicate that the property is non-configurable.
I am not attached to the specifics of my "object"/"public" block. But what this thread has already taught me is that it is instances that need sugar, not classes. Enhancing the object literal notation is one way to get there.
If there is to be inheritance (not necessarily single inheritance),
and something like Java's "protected" modifier for subclass-only access, then how would that fit into your system?
See the first message of the "Look ma no this" thread where I presented a similar scheme that could do single inheritance or linearlized multiple inheritance. (Of course, use of instanceof for nominal typing can only do single inheritance.) I light of the lack of interest in inheritance, I have dropped that aspect from further discussions.
On Sat, Nov 29, 2008 at 9:49 PM, Mark S. Miller <erights at google.com> wrote:
[1,2,3) Integrity]
It seems more integrity is desired. Programmers can use ES3 with discipline but it seems folks don't trust other folks or that "the junior programmer" will exercise the right amount of discipline. Programmers won't just stick to the documented API and won't just leave other people's objects alone. It is unfortunate. As far as I know, Gmail was written using only underscore naming convention for privacy so discipline can work.
- Minimize new kernel semantics: As demonstrated by various proposed desugarings, ES3.1 already contains adequate mechanism for the kinds of high integrity programming people expect from ES-H classes. Ideally then, classes would add no new kernel semantics at all to ES-H, but merely desugar to other elements already present. (The one exception we will probably make is more direct support for nominal types and type constrained variables and properties.)
ES3 did not expose enough control to the programmer so that he could write the desugared version of a class. ES3.1 has exposed more with things like Object.freeze. Perhaps more pieces may need to be exposed. I realize this is different than adding new kernel semantics but I think it is worth noting the difference.
I don't like the idea that types and type checking are being or might be conflated with classes. Nominal type checking systems, casting, and general fighting with the compiler to get otherwise valid code to work is a nightmare. These things make writing code harder and more painful. I am vary wary of type checking and if such a system makes it into ES, when I have to deal with third party code that uses type checking poorly, I sure a part of my soul will die. "Optional" type checking won't actually be optional. Its impact will be endured when dealing with code that uses type checks.
- Avoid accident prone constructs: Moderate. On the positive side, because self is simply a lexically captured variable, extracted methods are already bound to their instance without any new mechanism. Because they do not mention 'this', little damage arises if a function/method/constructor is invoked as a constructor/function/method, etc. OTOH, All five properties are enumerable though probably only the two data members should be.
I like the use of "self" in this example and it makes for easier programming. Any of the methods could be used as an event handler without worrying about dynamically bound "this". Surely this is a major hump for all programmers who start with JavaScript and want to write
setTimeout(obj.foo, 100);
and the "foo" method uses "this".
I also posted an example to this list about creating observable objects which benefit from not using "this".
[In a follow-up, I noted how there were an bad set of parens after "fire" in one place.]
Avoiding "this" is a major benefit, in my opinion.
Minimize new kernel semantics: Perfect.
Syntax as user interface: Good. I would have given this one a "Perfect" but for your point about access public members from inside as simple variable accesses. Obviously, one could instead write
function getX() { return privX; } const self = { ... getX: getX, ...
but this isn't perfect either. I'll come back to this in the next email.
I agree, this is not good "syntax as user interface". This is what I was addressing in my longer desugared code where "self" is aggregated automatically when "public" is written.
If making a public method means explicitly adding a property to an object then your proposed sugar is only nutrasweet ;-) I mean that jokingly. I understand you are trying to make a minimal proposal to see how minimal it can be.
Indeed. A sweet syntax that doesn't cause the growth of fat! ;)
Yeah, but that aftertaste!
I am not attached to the specifics of my "object"/"public" block. But what this thread has already taught me is that it is instances that need sugar, not classes. Enhancing the object literal notation is one way to get there.
That is interesting. I think the general feeling was that constructors were what needed the sugar. The fact that there are several ways to create objects in ES causes a bit of a problem here. I'll definitely think on this. Perhaps both object literals need more expressive powers and constructors need sugaring.
I think that my example covers the points you are making except for the "self" issue which has a reasonable easy boiler-plate-like fix.
When calling "new Point(1, 2)", it seems very consistent with ES that there will be a "this" object in the Point constructor (i.e. function or sugared class.) If the programmer wants a lexically bound "self" then one line at the beginning of the class will take care of that.
class Point(privX, privY) { var self = this; let privInstVar = 2; const privInstConst = -2; public toString() '<' + x() + ',' + y() + '>'; public get x() privX; public get y() privY; public pubInstVar = 4; public const pubInstConst = -4; }
This allows the toString line to be written as
public toString() '<' + x() + ',' + y() + '>';
or
public toString() '<' + self.x + ',' + self.y + '>';
The issue about a dynamically bound "this" or a lexically bound "self" is only an issue in this case if a method is "borrowed" like in the setTimeout example above. That is, a method called without it being a message passed to the receiver object. With the "var self=this;" line, a designer of a class would be able to choose how method borrowing will work. No I don't think this "this"/"self" solution is ideal but I don't think it is awful either given how "this" is used in ES now.
Peter
On Sat, Nov 29, 2008 at 9:49 PM, Mark S. Miller <erights at google.com> wrote:
var self = { to toString() { return '<' + self.getX() + ',' + self.getY() + '>'; }, to getX() { return x; }, to getY() { return y; } let pubInstVar: 4, const pubInstConst: -4 }; return self;
The "to" isn't a typo, but wasn't explained. It creates a non-writable, non-enumerable, non-configurable property whose value is a frozen function.
What is the motivation for choosing the string "to"?
I am not attached to the specifics of my "object"/"public" block. But what this thread has already taught me is that it is instances that need sugar, not classes. Enhancing the object literal notation is one way to get there.
The object literal notation shouldn't be burdened with freezing the actual function object. The object literal should at most allow specifying that it's own property reference is frozen. The object literal freezing the function object itself seems like going a step to far. For example, what if the property value is an object with yet another object nested inside? Would the top object literal allow freezing of the whole deep structure? That seems uncomfortable to me.
Peter
On Nov 30, 2008, at 1:11 PM, Peter Michaux wrote:
On Sat, Nov 29, 2008 at 9:49 PM, Mark S. Miller <erights at google.com>
wrote:var self = { to toString() { return '<' + self.getX() + ',' + self.getY() + '>'; }, to getX() { return x; }, to getY() { return y; } let pubInstVar: 4, const pubInstConst: -4 }; return self;
The "to" isn't a typo, but wasn't explained. It creates a non- writable, non-enumerable, non-configurable property whose value is a frozen
function.What is the motivation for choosing the string "to"?
IIRC, Mark's E language used that preposition. Think "in order to..."
before the verb-phrase-named function.
It may grow on you -- I see its intent, but observe that others such
as Peter miss it because "to" is an overloaded term in English and in
JS (to toString shows two among the plurality of meanings).
What's more, 'to' seems unnecessary in JS object initialisers, given
the unambiguous property name syntax. Instead of : after the property
name (which could be an identifier, number, or string), a ( begins a
formal parameter list.
ES4 allowed const and var before the property initialiser, to specify
DontDelete and optionally ReadOnly, as opposed to neither for the
property initialisers in ES3. In this light, var is better than let.
Using let before a property name in an object initialiser might
plausibly create a "lexical" member of the new object, whatever that
could be. It wouldn't be a property of the kind defined by var or
const. But without an implicit closure I don't see how to do this, and
EIBTI.
Peter Michaux wrote:
On Sat, Nov 29, 2008 at 9:49 PM, Mark S. Miller <erights at google.com> wrote:
var self = { to toString() { return '<' + self.getX() + ',' + self.getY() + '>'; }, to getX() { return x; }, to getY() { return y; } let pubInstVar: 4, const pubInstConst: -4 }; return self;
The "to" isn't a typo, but wasn't explained. It creates a non-writable, non-enumerable, non-configurable property whose value is a frozen function.
What is the motivation for choosing the string "to"?
It is used in E to introduce a method.
(It was also used in Logo to introduce a procedure declaration, but that may be a coincidence.)
I am not attached to the specifics of my "object"/"public" block. But what this thread has already taught me is that it is instances that need sugar, not classes. Enhancing the object literal notation is one way to get there.
The object literal notation shouldn't be burdened with freezing the actual function object. The object literal should at most allow specifying that it's own property reference is frozen. The object literal freezing the function object itself seems like going a step to far.
The problem is that functions are mutable, which we are stuck with for backward compatibility. But there is no such backward compatibility constraint on lambdas, which can and should be immutable. So, given Allen Wirfs-Brock's syntax for lambda, we can do something like:
var self = { method toString: {|| '<' + self.getX() + ',' + self.getY() + '>'}, method getX: {|| x}, method getY: {|| y}, field pubInstVar: 4, const pubInstConst: -4, }; return self;
where the extensions to the object literal syntax used here are:
- a 'field' modifier to declare a property non-[[Configurable]],
- a 'const' modifier to declare a property non-[[Configurable]] and non-[[Writable]],
- a 'method' modifier to declare a property non-[[Configurable]], non-[[Writable]] and non-[[Enumerable]].
Note that modifiers on object literal properties do not need to be reserved keywords ('get' and 'set' are already in this category).
On Mon, Dec 1, 2008 at 12:01 PM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:
Peter Michaux wrote:
What is the motivation for choosing the string "to"?
It is used in E to introduce a method.
(It was also used in Logo to introduce a procedure declaration, but that may be a coincidence.)
Not a coincidence. E's use of "to" was inspired by Logo's.
The problem is that functions are mutable, which we are stuck with for backward compatibility. But there is no such backward compatibility constraint on lambdas, which can and should be immutable. So, given Allen Wirfs-Brock's syntax for lambda, we can do something like:
var self = { method toString: {|| '<' + self.getX() + ',' + self.getY() + '>'}, method getX: {|| x}, method getY: {|| y}, field pubInstVar: 4, const pubInstConst: -4, }; return self;
where the extensions to the object literal syntax used here are:
- a 'field' modifier to declare a property non-[[Configurable]],
- a 'const' modifier to declare a property non-[[Configurable]] and non-[[Writable]],
- a 'method' modifier to declare a property non-[[Configurable]], non-[[Writable]] and non-[[Enumerable]].
Note that modifiers on object literal properties do not need to be reserved keywords ('get' and 'set' are already in this category).
I like this a lot. However, I think using lambdas directly for methods has a fatal flaw: wrong "return" binding. As Waldemar pointed out at Kona, lambdas return their completion value, making them hazardous for direct use in contexts other than control abstractions. When used as a method, a lambda written only to bring about side effects may inadvertently leak a shoulda-been-encapsulated-value to its caller.
If someone writing a lambda is thinking "method", they may very well write a return, which will actually cause a return from the closest enclosing function. E has the same two levels of we expect for Harmony: return binding vs completion value, where the first expands to the second. But we generally use only the return-binding form for methods, to avoid this leakage hazard. (As you recall, we learned this lesson the hard way ;).)
On Mon, Dec 1, 2008 at 12:01 PM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:
var self = { method toString: {|| '<' + self.getX() + ',' + self.getY() + '>'}, method getX: {|| x}, method getY: {|| y}, field pubInstVar: 4, const pubInstConst: -4, }; return self;
where the extensions to the object literal syntax used here are:
- a 'field' modifier to declare a property non-[[Configurable]],
- a 'const' modifier to declare a property non-[[Configurable]] and non-[[Writable]],
- a 'method' modifier to declare a property non-[[Configurable]], non-[[Writable]] and non-[[Enumerable]].
I don't think only a "method" would have the combination of attributes listed above. Also because functions are first class in JavaScript, I think of a function valued property as a field so don't like the name "field". Also would "method" be able to be a lambda or function?
There are 2^3 = 8 combinations of Configurable, Writable, Enumerable and potentially more attributes in the future. I'd prefer to control them independently. If multiple prefix modifiers are not appealing, what about some flags like the 'g' that can follow a regexp literal (e.g. /a/g)
var self = { toString[]: {|| '<' + self.getX() + ',' + self.getY() + '>'}, getX[]: {|| x}, getY[]: {|| y}, pubInstVar[WE]: 4, pubInstConst[E]: -4, };
If no brackets are included then it would be equivalent to [WEC] as that is backwards compatible (I think).
I'm not sure I like this idea. It is just an idea.
Peter
On Tue, Dec 2, 2008 at 1:54 PM, Peter Michaux <petermichaux at gmail.com> wrote:
On Mon, Dec 1, 2008 at 12:01 PM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:
var self = { method toString: {|| '<' + self.getX() + ',' + self.getY() + '>'}, method getX: {|| x}, method getY: {|| y}, field pubInstVar: 4, const pubInstConst: -4, }; return self;
where the extensions to the object literal syntax used here are:
- a 'field' modifier to declare a property non-[[Configurable]],
- a 'const' modifier to declare a property non-[[Configurable]] and non-[[Writable]],
- a 'method' modifier to declare a property non-[[Configurable]], non-[[Writable]] and non-[[Enumerable]].
I don't think only a "method" would have the combination of attributes listed above.
Agreed. The proposed names do not denote what they appear to denote.
On Tue, Dec 2, 2008 at 2:35 PM, Jon Zeppieri <jaz at bu.edu> wrote:
... I do want to point out, however, that one important combination is omitted in David-Sarah's proposal (modifier names aside). Private instance variables would likely be represented as non-configurable, non-enumerable, yet writable properties. I'm guessing that 'x' and 'y' (which are suggested by, but left out of the examples above) would fit that description.
OK -- I just revisited the older messages in the thread. 'x' and 'y' are lexically bound (by the constructor function). So, ignore the above...
On Tue, Dec 2, 2008 at 11:35 AM, Jon Zeppieri <jaz at bu.edu> wrote:
var self = { toString[]: {|| '<' + self.getX() + ',' + self.getY() + '>'}, getX[]: {|| x}, getY[]: {|| y}, pubInstVar[WE]: 4, pubInstConst[E]: -4, };
I find this ugly, but I don't have any great ideas. I do want to point out, however, that one important combination is omitted in David-Sarah's proposal (modifier names aside). Private instance variables would likely be represented as non-configurable, non-enumerable, yet writable properties. I'm guessing that 'x' and 'y' (which are suggested by, but left out of the examples above) would fit that description.
x and y above are indeed private instance variables, but that's precisely because they are not properties -- they are normal variables lexically captured by the functions/lambdas which use them.
On Tue, Dec 2, 2008 at 1:54 PM, Peter Michaux <petermichaux at gmail.com> wrote:
var self = { toString[]: {|| '<' + self.getX() + ',' + self.getY() + '>'}, getX[]: {|| x}, getY[]: {|| y}, pubInstVar[WE]: 4, pubInstConst[E]: -4, };
If no brackets are included then it would be equivalent to [WEC] as that is backwards compatible (I think).
I'm not sure I like this idea. It is just an idea.
Another "just an idea":
#{ ... } is an object initializer where all of the initialized properties are non-configurable. 'enum' (or something similar) is a property modifier that makes the property enumerable. 'const' continues to have the same meaning (non-configurable and non-enumerable). Where 'const' appears inside #{ ... }, the non-configurable aspect is redundant.
var self = #{ const toString: {|| '<' + self.getX() + ',' + self.getY() + '>'}, const getX: {|| x}, const getY: {|| y}, enum pubInstVar: 4, enum const pubInstConst: -4 };
It's more verbose than Peter's syntax, but it doesn't overload the array index syntax, and although the #{ ... } initializer unfortunately introduces new syntax, I think its functionality is genuinely (and generally) useful. I'd imagine that most real-world uses of { ... } could be replaced by #{ ... }, since initialized properties are usually expected to stick around.
On the other hand:
enum const get foo () { ... }
... is odd.
On Tue, Dec 2, 2008 at 1:40 PM, Jon Zeppieri <jaz at bu.edu> wrote:
#{ ... } is an object initializer where all of the initialized properties are non-configurable.
The use of '#' makes me think of reader macros. I like the idea of reader macros for ES. (I don't know if '#' can be used because of sharp variables which I believe are part of JavaScript.)
I think it would be good to be able to use custom quotes on regular expressions and strings
#r(some/regexp/containing/slashes)
#s/a string containing " quotation ' marks/
'enum' (or something similar) is a property modifier that makes the property enumerable. 'const' continues to have the same meaning (non-configurable and non-enumerable). Where 'const' appears inside #{ ... }, the non-configurable aspect is redundant.
var self = #{ const toString: {|| '<' + self.getX() + ',' + self.getY() + '>'}, const getX: {|| x}, const getY: {|| y}, enum pubInstVar: 4, enum const pubInstConst: -4 };
It's more verbose than Peter's syntax, but it doesn't overload the array index syntax,
I used [] just as an example. It could have been many different separators.
and although the #{ ... } initializer unfortunately introduces new syntax, I think its functionality is genuinely (and generally) useful. I'd imagine that most real-world uses of { ... } could be replaced by #{ ... }, since initialized properties are usually expected to stick around.
On the other hand:
enum const get foo () { ... }
... is odd.
I think if there are to be modifiers than just have the modifiers and not the #{}. Having all the modifiers with the property seems like a better way to go to me. Also what if #{} is used to create the object and later a property is added and is to be configurable? Is that possible? Either decision seems arbitrary.
If there are going to be modifiers "config", "enum", and "const" then there may be no harm in having other modifiers which represent a combination of these modifiers (like the suggestions of "field" and "method" though I don't really like those names for the reasons I wrote before.)
Peter
On Tue, Dec 2, 2008 at 4:58 PM, Peter Michaux <petermichaux at gmail.com> wrote:
I think if there are to be modifiers than just have the modifiers and not the #{}. Having all the modifiers with the property seems like a better way to go to me.
That seems fine to me, too. The #{ ... } idea just struck me, because I realized that it pretty well captures what I actually mean when I use { ... }.
Also what if #{} is used to create the object and later a property is added and is to be configurable? Is that possible? Either decision seems arbitrary.
Yes, and I don't consider that arbitrary, at all. The meaning of
obj.foo = bar;
can't be dependent upon the mechanism used to create obj.
Now, if #{ ... } were to seal the object, then obviously you couldn't add a new property to it. While sealed (and frozen, for that matter) initializers would be useful in places, just enforcing non-configurability of the initial set of properties seems like a reasonable trade-off between general usefulness (like I said, I bet that most uses of { ... } could be replaced by #{ ... } without a problem) and optimizer-friendliness. (I'm thinking of Mark's number (6) from earlier in this thread.)
Of course, there could also be initializers for sealed and frozen objects. E.g., #s{ ... } and #f{ ... } -- or whatever. And these would be more optimizer-friendly and less generally useful than #{ ... }. And each would introduce new syntax. I'm in the "minimize new syntax" camp.
If there are going to be modifiers "config", "enum", and "const" then there may be no harm in having other modifiers which represent a combination of these modifiers (like the suggestions of "field" and "method" though I don't really like those names for the reasons I wrote before.)
Well, I think it's just a good practice to keep the set of modifiers (and new syntax, in general) small.
This is the message from David-Sarah I was referring to, that uses lambda to express a pleasant object literal, but with a leakage hazard.
---------- Forwarded message ---------- From: David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk>
Date: Mon, Dec 1, 2008 at 12:01 PM Subject: Re: How much sugar do classes need? To: es-discuss at mozilla.org
Peter Michaux wrote:
On Sat, Nov 29, 2008 at 9:49 PM, Mark S. Miller <erights at google.com>
wrote:
var self = { to toString() { return '<' + self.getX() + ',' + self.getY() + '>'; }, to getX() { return x; }, to getY() { return y; } let pubInstVar: 4, const pubInstConst: -4 }; return self;
The "to" isn't a typo, but wasn't explained. It creates a non-writable, non-enumerable, non-configurable property whose value is a frozen
function.
What is the motivation for choosing the string "to"?
It is used in E to introduce a method.
(It was also used in Logo to introduce a procedure declaration, but that may be a coincidence.)
I am not attached to the specifics of my "object"/"public" block. But
what
this thread has already taught me is that it is instances that need
sugar,
not classes. Enhancing the object literal notation is one way to get
there.
The object literal notation shouldn't be burdened with freezing the actual function object. The object literal should at most allow specifying that it's own property reference is frozen. The object literal freezing the function object itself seems like going a step to far.
The problem is that functions are mutable, which we are stuck with for backward compatibility. But there is no such backward compatibility constraint on lambdas, which can and should be immutable. So, given Allen Wirfs-Brock's syntax for lambda, we can do something like:
var self = { method toString: {|| '<' + self.getX() + ',' + self.getY() + '>'}, method getX: {|| x}, method getY: {|| y}, field pubInstVar: 4, const pubInstConst: -4, }; return self;
where the extensions to the object literal syntax used here are:
- a 'field' modifier to declare a property non-[[Configurable]],
- a 'const' modifier to declare a property non-[[Configurable]] and non-[[Writable]],
- a 'method' modifier to declare a property non-[[Configurable]], non-[[Writable]] and non-[[Enumerable]].
Note that modifiers on object literal properties do not need to be reserved keywords ('get' and 'set' are already in this category).
-- David-Sarah Hopwood
In the "Look ma, no this" thread < esdiscuss/2008-August/thread.html#6941>,
I presented a desugaring of classes without proposing what the sugared syntax should be. This desugared form is based on the objects-as-closure style pioneered by Crock. The resulting thread explored some possible sugarings as well as alternative desugarings. At < strawman:classes>, Cormac presents an
approach to the sugar that we discussed at the Kona EcmaScript meeting. Some details aside, this looks like it could all be desugared straightforwardly to the objects-as-closure style.
However, in the (small number of days) since Kona, I've become less happy with the degree of sugar involved. For some elements of the desugared form, the proposed sugar is significantly more convenient, but not for others. When the sugar doesn't add enough convenience to pay for its complexity, we should leave it out. Unnecessary sugar obscures what's "really" going on (one level of abstraction down), making it harder to form a good cognitive model of the language. (As Alan Perlis says, "To much syntactic sugar leads to cancer of the semicolon.")
Here's an only slightly sweetened alternative. I hope it doesn't leave a bitter aftertaste. Let's start with a desugared example expressing most of the features that the sugared class propossal wishes to address:
const Point = let { // after desugaring let privClassVar = 1; const privClassConst = -1; Point.pubClassConst = -3;
};
Note that the freezing of Point also freezes Point.prototype, so afterwards "x instanceof Point" is a sound and monotonic nominal type check. Whether "x :Point" should therefore do an instanceof check I will leave to a separate discussion.
Sugaring only the elements above that need it, one possibility is:
EcmaScript historically started by trying to unify the notions of scoped variables and inherited properties. This was a noble effort, but failed. At every step, we found that this attemped unification led to trouble. My main discomfort with the proposed sugar on the wiki page is that it would obscure this distinction yet again. Above I have tried to present a minimalist sugar by example that is no less convenient, but which always makes visible in the sugared form the distinction between variables and properties in the desugared form.