-
Notifications
You must be signed in to change notification settings - Fork 3
AWB alternative 2 - instance vars and secret methods #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I need more time to think about it but with a little bikeshedding this could be something... Speaking of which, the Could we get away with a keyword without breaking the user's intuition regarding |
Well I really like using the same symbol sequence for both the actual selection operator and the sigil that distinguishes a property based concise method from a "secret" concise method. It really ties the two together. The problem with a keyword is that the actual word you choose can carry too much implied meaning and clutter. And, how much push back to we get from the generator *. But, if we really wanted a keyword why not "secret" I also considered "hidden" as terminology before I went with "secret" in the write-up. It might actually be better. And, of course we could try .. instead of ->. Initially I liked foo..bar better because is was so similar to foo.bar. But -> grew on me. Mess chance of a type stutter and the distinction between the two operator is big enough that it may be worth a bit more emphasis. You could try some examples using just those variations. I did a bunch of that various class forms in the early ES6 days. In the end, there just wasn't that much meaningful different between various forms. |
I've updated the examples to use this syntax, with I think a class C {
->*foo() {}
->async foo() {}
->async *foo() {}
} I thought "secret" might be a little too strong (or James Bond-esque), but "hidden" seems to provide the right connotation. Since hidden methods can be called with any object as the receiver, it appears that they are completely sufficient for "static private": class C {
hidden factoryA() {
return new this('a');
}
hidden factoryB() {
return new this('b');
}
static factory(kind) {
return kind === 'A' ? this->factoryA() : this->factoryB();
}
} Do you agree? This seems at the same time both wonderful and surprising to me! |
One aspect that might stand out to people is the lack of initializer expressions on instance variables. I agree that we don't need them and the design is simpler without them. Another benefit of leaving initializers out is that the installation of instance variables is atomic: an instance either has all of them (defined by a single class) or none of them. I've desperately wanted that ever since starting to work on private state. Even if we add initializer expressions in a future update, I think that we ought to retain that property. |
what I had in mind was more along these lines: class C {
*->foo() {}
async ->foo() {}
async *->foo() {}
} the -> always directly precedes the name. I'm beginning to like the "hidden" terminally more than "secret", regardless of whether we use it as a keyword or just as terminology. The main issue WRT "static" hidden methods is the Home object binding if they use But, for me, having static hidden methods is a point in favor of the sigil instead of the keyword:
|
Yes, I prefer to leave the initializers out and require the declarations (no creation of instance vars by assignment) I think we need to be careful of feature-creep. One of the strongest arguments agains the current set of proposed class extensions is the overall complexity when you include everything. |
If we go the keyword route, we could allow either I think (similar to the method modifiers in C#):
Since
Option 2 sounds somewhat nice to me in an abstract way, since these methods are kind of "homeless": they aren't actually attached to any objects. What would be the disadvantage of not having a Home (and thereby disallowing super)? I suppose that it could make it more difficult to transparently refactor normal methods that contain super references. On the other hand, option 1 will probably be more intuitive to users. |
I'd go with option 1. I like the simplicity of being able to say any concise methods (that includes static methods) can be turned into a hidden method, and it doesn't change the behavior of the method. For people who don't use Allowing arbitrary ordering of class modifiers doesn't help the simplicity story very much. Programmers still have to pick an ordering and if they settle on using a specific ordering and they suffer some uncomforted each time they read code that uses a different order. And it certainly makes the specification more complex because it has to deal with the possibility of multiple occurrences of a modifier. I still think we should lead with the name sigil and save the |
Max-min classes 1.1. AWB alternative 2aThis is the same basic proposal at that at the head of this issue thread, but with terminology and other updates based upon the above discussion. Here is a proposal that is closer to the @zenparsing parsing original, but perhaps simpler. With this proposal, I don't necessarily see the need for a classes 1.2 follow-on The starting point for this proposal are ECMAScript class definitions as defined in the ECMAScript 2018 Language Specification. No, other proposed extensions are assumed or required. Basic Concepts
Exampleclass Point {
var x; //or: var x, y;
var y;
->brandcheck() {
try {this->x} catch {return false};
return true;
}
get x() {return this->x} //normal accessor properties
get y() {return this->y}
add(another) { //a normal method property
if (another.brandcheck())
return new Point(this->x+another->x, this->y+another->y);
else
return new Point(this->x+another.x, this->y+another.y);
}
constructor (x,y) {
this->x = x;
this->y = y;
}
} Technical Notes and Rationale
|
This is looking great - thanks! I must admit a strong preference for the keyword here. It seems to just slide right in to the current class aesthetics. It "sings" to me. If we do end up going with "hidden", and we want a specific order, then I'm leaning towards "static hidden". Bikeshedding aside, the only other issue I have has to do with the interaction between normal lexical scope and instance var names. Consider: var x = 1;
class C {
var x;
constructor() {
console.log(x);
}
}
new C(); // Logs "1" Similarly: var x = 1;
class C {
var x;
constructor() {
console.log(eval("x"));
}
}
new C(); // Logs "1" This seems surprising to me. Do you think we can use early errors or uninitialized bindings in the class body scope (or both!) to eliminate such cases? |
Well, it would be no different if you had written: let x = 1; // <-- changed var to let
class C {
var x;
constructor() {
console.log(x);
}
}
new C(); // Logs "1" or class C {
var x;
constructor(x) { // <----- added parameter
console.log(x);
}
}
new C(1); // Logs "1" These example helps illustrate that there are two key things that people who use class definitions have to learn:
I don't believe the var x = 1;
class C {
var x;
constructor() {
console.log(eval("this->x"));
}
}
new C(); // Logs "1" Should this be legal? Probably. Eval has special early error rules for new.target, SuperProperty and SuperCall that ensures that they only occur in evals that are within an appropriate kind of method. Interestingly, it is less clearly that Regarding, |
Yes, this is the hangup. When I let go of my vestigial concept of On the other hand, I'm not 100% convinced yet that we can't have it both ways. I think that given the history of |
We could have The nice thing about the instance variable concept it is explicitly labeling x as a specific kind of thing rather then just emphasizing it's visibility while leaving what kind of thing x is implicit. Even if we could make unqualified access to instance variables work in JS (and I very dubious) it's still a complication. To have any change with this proposal I think we have to focus on simplicity. |
That's exactly my worry with using Also, from an aesthetic standpoint, I like the fact that with the current proposal it is easy to simply glance at a class definition and understand the internal storage requirements for instances, independent of any surface API or internal code organization. |
@allenwb Clearly we should disallow hidden property names that are string literals, numeric literals, or computed property names. But should we allow PropertyNames as hidden method names, or just identifiers? class C {
hidden delete() {}
method() { this->delete() }
} This seem fine to me (I think). |
@zenparsing yes, fine with me too. I guess, we could allow string and numeric literals as they are statically know values. But since we at least have to restrict computed property names I think it is probably best to go all the way and say a hidden names (both method and inst vars) be be a valid IdentifierName |
@allenwb This is a bit in the weeds, but I'm trying to work out the model more clearly: In order to resolve hidden method names we'll need to internally resolve property names to property descriptors (because the hidden method could be an accessor). From a specification point of view, do you think it makes sense to use lexical environments for storing these mappings? Currently, lexical environments are only used to resolve identifiers, and their values are "ECMAScript language values". Another option would be to invent a new environment-like specification type that would carry the hidden method scope chain. A more radical approach might be to re-use the existing object model: hidden methods could be stored on a "hidden object" whose prototype is the next level up in the hidden method scope chain. |
I'd avoid getting Property Descriptors involved. I'd start with the work in the current "private fields" proposal that extends lexical environments with a parallel name space for private name bindings. (But, I actually haven't look at the algorithms of that proposal for a while, so it's possible it is no longer a good starting point). I'd define the hidden name namespace, such that the bound value of a hidden name is a "hidden member descriptor". There are 3 kinds of HMDs: Instance Variable: {id: definitionID, name: string} Where definitionID is a unique value assigned to each class (or object literal) definition evaluation that defines instance variables. If we have static instance variables then there is a separate definitionId for the those. Each ordinary object has an internal slot [[InstanceVariables]]. The value of that slot is a list whose elements are of the form {id: definitionID, vars: instanceVars}. instanceVars is a list whose elements are of the form {name: string, value: ECMAScript language value}. (note there are various way we could structure this, but I think this design leads implementation in the direction I want them to think about instance variables. The semantics of |
Thanks, your suggestions have been extremely helpful! I want to come back to this for a minute:
I agree 100% with you here. But I'm increasingly worried that when we present this idea to others they will be strongly attracted to Consider the integration of this proposal with TypeScript. One of the great features here is that hidden methods integrate so smoothly: // TypeScript!
class Something {
public foo: string;
constructor() {
this.foo = 'lkasjdf';
this->helper();
}
hidden helper() {
console.log('help');
}
} Now let's add instance variables: // TypeScript!
class Something {
var bar: boolean;
public foo: string;
constructor() {
this.foo = 'lkasjdf';
this->bar = true;
this->helper();
}
hidden helper() {
console.log('help');
}
} Not bad, but now let's try using // TypeScript!
class Something {
hidden bar: boolean;
public foo: string;
constructor() {
this.foo = 'lkasjdf';
this->bar = true;
this->helper();
}
hidden helper() {
console.log('help');
}
} They are going to love that. But if we go that direction (using Of course, I don't think we should be designing for the benefit of TypeScript. (In fact, I find the amount of pressure being exerted on JS by TS quite troubling.) But the same arguments apply to developers using "fields" with Babel. It seems to me that we have two options:
Thoughts? |
I really want to firmly plant the concept that Instance variables are a fundamentally new concept (for JS) that enables strict encapsulation of state within objects. Our slogan: Instance variables are not properties. This distinction is so important that we use the keyword |
I would really like to be able to strongly position that instance variables are a new fundamental part of the ES object model and are not exclusive tied to class declarations. We should be able to use them for strictly encapsulated state without using a class definition. But, to make that argument I think we need to allow them to be defined within object literals. EG: function Point(x,y) {
return {
var x: x,
var y: y,
get x() {return this->x},
get y() {return this->y),
//...
}
} seems like it should be roughly equivalent to: class Point {
var x;
var y;
constructor(x, y) {
this->x = x;
this->y = y;
}
get x() {return this->x}
get y() {return this->y)
//...
} but they are really quite different in at least one important way which becomes apparent if you try to add this method to both formulations: plus(aPoint) {
let newX = this->x + aPoint->x;
let newY = this->y + aPoint->y;
//...
} This works for the class case but fails when invoking the I suspect that this difference could be a significant foot-gun. (I've had this concern about the various lexically scoped class private proposals for a long time, but let it slip by because with classes it would really only arise with class expression whose use is fairly uncommon). I find foot-gun disturbing enough that I'm inclined to completely leave object literals out of the classes 1.1 proposal. But I'm loath to throw away what I think is a strong positioning argument that instance variables are a fundamental feature of the ES object model and not just a feature of class definitions. However, I just posted a little twitter rant about "kick the ball down the field" language design and that it often a symptom that some fundamental issues are being ignored. I also think that the current class enhancement proposals seriously suffers from that syndrome. So, I at least want to think about the problem before kicking the ball. Here is the best solution I've come up with so far: the assignment of definitionIDs (c/f my up-thread semantics sketch) for object literals are "hoisted" to the enclosing function level (similarly to the scoping of BTW, I'm not (yet) proposing that we do this sort of "hoisting" for class definition. I need to think about it more. However, I think it may also work just fine with them and would also address by class expression concerns. |
Very interesting idea. I'm curious about this example: function f() {
return function g() {
return {
var x: 0,
add(other) { this->x += other->x },
};
};
}
let a = f()();
let b = f()();
a.add(b); // Does this work, or throw? Is the definitionID a dynamic property of the scope, and different for each function object, or is it a static property of the parse node representing the function? I'm glad you brought up class expressions and I think you've identified a problem with the current encapsulation semantics. Check out the mixin pattern described here. The basic idea is: let M = Base => class extends Base {
// ...
};
class A {}
class B extends M(A) {} It seems clear that private access will not work across different let M = Base => class extends Base {
var x;
constructor() { this->x = 1 }
plus(other) { this->x += other->x }
};
class A {}
class B extends M(A) {}
class C extends M(A) {}
new B().plus(new C()); // Throws? |
as I (intended to) described it above, it would throw because each invocation of function f would create a new function object g and definitionIDs would have the extent of their containing (non-arrow) function object. I have also considered the parse node association but for this thread went with the function level association as it provides a way to get per object literal definitionIDs (by wrapping { } in an IIFE) if that is what you really wanted. I'm also aware (and very supportive) of this mix-in pattern and aware that it has the same problem. It's largely what I was thinking about as a "less common use case". In particular, the current private fields proposal has this exact same problem and nobody seems to care. Also, there is a work around for mix-ins: class A {}
class AWithM extends M(A) {}
class B extends AWithM {}
class C extends AWithM {}
new B().plus(new C()); //works as intended (assuming => doesn't create a new definitionID context I believe that I briefly talked about this issue with @erights (Mark, are you listening??) sometime last year and suggested the parse node solution. My recollection was that Mark was very uncomfortable with any violation of the normal expectations of closure capture and I didn't pursue it further. |
I don't have any strong opinions yet, but here are some random thoughts: I would also (I think) be uncomfortable with a parse node variant. If I wanted to create a self-contained object graph factory: function createWorld() {
class A { /* ... */ }
class B { /* ... */ }
return { A, B };
}
let world1 = createWorld();
let world2 = createWorld();
let a1 = new world1.A();
let a2 = new world2.A(); then I would probably expect classes for different "worlds" to be incompatible with each other. On the other hand, I'm worried that the "containing function" variant would be a little too subtle. I mean, it's easy enough to explain that I sympathize with the desire to position instance variables as a fundamental new feature of the object model. They certainly are. But there's also, I think, a good reason why we don't need to provide this capability outside of class instances. For singleton object definitions, we can simply use closure to approximate internal slots: methods are per-instance rather than shared and "per-instance" state can be stored in local variables. It's only when we want to share methods across instances using the prototype chain that the need for an instance variable syntax arises. Again, just some random thoughts on a Friday afternoon. |
I was looking at this comment on the static features repo, specifically:
This seems to me to be another footgun related to having instance variables installed on singletons. Generally speaking, we don't want to provide a "private state" mechanism on objects that are intended to be used as prototypes for other objects. And I think objects created from object literals should probably be suitable for usage as prototypes for other objects. To me, this seems like a pretty good argument against supporting instance variables on object literals and class constructors. |
(I'm posting before I absorb everything, so some posts may already be addressed. Here goes.) I find the example at #7 (comment) compelling: var x = 1;
class C {
var x;
constructor() {
console.log(x);
}
}
new C(); // Logs "1" Why not keyword ->x; for some appropriate keyword? At this stage of the thread, all the other private declarations are prefixed with a unary With this suggestion, the keyword could even be var x = 1;
class C {
private ->x;
constructor() {
console.log(x);
}
}
new C(); // Logs "1" |
Where does |
What "field" concept do you refer to? Is this distinct from both properties and private slots? Why is it inevitable? |
Just noting that the WeakMap-like model of private state makes that even clearer. By contrast, putting the state in so-called "internal properties" muddies exactly these waters. The following text shows that we don't get to escape adding a new kind of internal bookkeeping thing either way. You invent Doesn't it seem weird that proxies have a [[InstanceVariables]], and that these operations on it must not cause the proxy to trap? The semantics is right. The problem is that we explain it in a way that makes it seem weird.
|
Sorry. I am now. Yes, I agree with both of your examples at #7 (comment) . We don't even need to use subclassing or parameterization to make the issue clear: function m() {
class M {
var ->x;
constructor() { this->x = 1 }
plus(other) { this->x += other->x }
};
return new M();
}
m().plus(m()); // must throw It must throw because the operands are instances of two different classes. |
I'll take this sub issue (per class or per parse node) as an opportunity to make a larger point. No matter what we do, some people will expect the other. The principle of least surprise will still leave some people surprised. If we choose per class, then the code written with the expectation of per parse node will fail safe. If we choose per parse node, then code written with the expectation of per class may seem to work under testing, but fail unsafe when exploited. We should be on the lookout for such asymmetries. |
I actually suggested that sort of prefixing for concise method, and Kevin convinced me to not go down that path. An of course, in usage, -> is a binary operator not a prefix. Primary reason is that the community response to the #x prefix has been very negative and it appears to be not just about the # character. The whole idea of prefixing variables seems to be disliked. Fear that we are starting down a BASIC style prefixing route. i originally suggested "secret" for the keyword with "hidden" as another possibility. We ultimately settled on "hidden" as a better word. Note that we are intentionally avoiding "private", "protected", etc because we don't want to clash with existing usage of those words by popular transpiler based extensions and dialects. Also, we intentionally did not go with |
The one in the current TC39 proposal of record. Where "field" means either an own property or a private instance slot. We are trying to simplify by not conflating those two things under one name. And not providing a new and redundant mechanism for defining and initializing instance own properties. |
I am fascinated by generalizing private state so that it can work for object literals. If we think of class M {
var ->x;
foo() { this->x = this->x +1; }
} as syntactic sugar for let M = (() => { . // IIFE
const %x% = new SlotMap();
return class M {
foo() { %x%.set(this, %x%.get(this) + 1); }
};
})(); then we might generalize to consider declaration F stuff {
var ->x;
return {
foo() { this->x = this->x +1; }
};
} as syntactic sugar for let F = (() => { // IIFE
const %x% = new SlotMap();
return declaration F stuff {
var ->x;
return {
foo() { %x%.set(this, %x%.get(this) + 1); }
};
};
})(); Suddenly, I feel good about function F(x) {
return {
var ->x = x;
foo() { this->x = this->x +1; }
};
} since class Point {
var ->x;
var ->y;
constructor(x, y) {
this->x = x;
this->y = y;
}
} could instead be written with initialization instead of assignment class Point {
constructor(x, y) {
var ->x = x;
var ->y = y;
}
} |
Note that this is terminology that we do not use. If you spot it tell us We consistently say "instance variable" |
It would be good to get some evidence on this. To my eyes, the |
What is [[InstanceVariables]] ? |
Read the multiple issue threads that have been filed against the current proposals with hundreds of supporting comments, and the continuous stream to twitter posts, and blog posts, and medium articles... If you pay attention to these sorts of community channels it's all over the place. |
Remember that many of those who complained, objected equally to not being able to use dot on use, like they can in statically typed languages or in TypeScript. Since we're committed to |
It the attachment point for associating instance variables with an object. If you want to think in terms of a weak map model and inverted weak maps, it corresponds to each objects collection of inverted map entries. |
One of the things we are trying to do with this proposal is seriously follow the max-min design path. Keep things as simple as possible, eliminate all not essential features and syntactic embellishments. Syntactically, G'night.... |
In general, in the spec, when an object has associated with it something using the notation [[Foo]], we say that [[Foo]] is an internal property. Is [[InstanceVariables]] an internal property? |
I find the visual scope confusion at #7 (comment) caused by |
Thanks for all the great feedback!
In order to help us avoid getting confused about the meaning of "property", let's use "internal slot" for things like The way we've currently specced it, Actually, I think using a more Map-like spec implementation might help JS educators in describing this feature.
Allen was also very interested in this idea, but I think I've convinced myself that we should not go there. I briefly mentioned my rationale in #7 (comment), but to restate: Instance variables are not appropriate for objects that are intended to be used as prototypes for other objects, because access to instance variables is not supported through the prototype chain. This is fine for instances of classes, and matches the behavior of instances of the built-ins. But currently, objects created with the object literal syntax are expected to work as prototypes for other objects. I believe that it would be surprising to many if the addition of an instance variable to an object literal broke its usage as a prototype: let obj = {
var x = 1,
m() { return this->x },
};
let child = Object.create(obj);
child.m(); // Throws, surprisingly?
I'm curious to know your thoughts on #18, which would eliminate that scope confusion (but possibly create others) and also provide users with a way to elide repetitive |
No, we don't use that terminology any move, starting with ES6 we say that [[Foo]] is an internal slot. See 6.1.7.2. After this proposal, we would probably call them "instance variables". |
In practice, I don't think that will occur very often. Programmer who know modern JS well enough to code a class with instance variables will also know that However, we could eliminate any such possibility, by defining a static semantic rules that says that it an early error to define a instance variable named Such rules would add some complexity to the spec. and that is something we are trying to avoid. But it could be done. Part of what we are reacting to with this proposal in the growing conceptual and actual complexity of recent proposals. Essential complexity is, well, essential. But accumulating inessential complexity is detrimental. It would be terrible if someone actually wrote something like: var x;
class foo {
var x;
get x() {return this->x}
} But is it worth adding complexity (and actual runtime work) to detect and reject it? |
Closing in preparation for public review (we want reviewers to focus on the proposal materials rather than long issue threads). Please feel free to continue discussion here or open a new issue for a specific topic. |
As my observation , there are two factor in dislike of
I believe
Note, This is also why I think BTW, I do a little prefer
|
Several comments from @allenwb and @zenparsing seem to be based around an idea that |
@littledan That is all we were saying, -> is an accessor operator that is different from the Regarding, using |
I quite like the idea of closure-like variables instead of having to worry about things like "writable"/"configurable" (like-properties) that the current private fields proposal would allow. It would be nice though if these are variable-like for them to be actually variable-like and allow class Vector(x, y) {
const ->x = x
const ->y = y
/* No constructor */
add(otherVector) {
return new Vector(this->x + other->x, this->y + other->y);
}
get x() {
return this->x
}
get y() {
return this->y
}
}
new Vector(3, 4); Another example with a constructor as well: class Adder extends HTMLElement {
const ->shadowRoot = this.attachShadow({ mode: 'closed' })
let ->count = 0
constructor() {
super();
this->shadowRoot.innerHTML = someTemplate;
this->shadowRoot.querySelector("#clickMe").onclick = _ => {
this->count += 1
this->render()
}
this->render()
}
hidden render() {
const countElem = this->shadowRoot.querySelector("#countOut")
countElem.innerHTML = this->count
}
} |
@Jamesernator About |
The main reason for suggested class-parameters is because class Adder extends HTMLElement {
constructor() {
const ->shadowRoot = this.attachShadow({ mode: 'closed' })
}
hidden render() {
// Is it confusing that `const ->shadowRoot` isn't block-scoped
// to the constructor function? Probably not to be honest
// after using it a couple times
const countElem = this->shadowRoot.querySelector("#countOut")
}
} I'll continue discussion on #25 though. |
Uh oh!
There was an error while loading. Please reload this page.
Max-min classes 1.1. AWB alternative 2
An updated version of this proposal is below at #7 (comment)
Here is a proposal that is closer to the @zenparsing parsing original, but perhaps simpler. With this proposal, I don't necessarily see the need for a classes 1.2 follow-on
Basic Concepts
undefined
.->
->
. It is up to the code of the method body whether or not a runtime error will occur if thethis
value is not an instance of the containing class or one of its subclasses. Access to instance variables that don't exist for thethis
value would, of course, throw.->
operator is not the lexically visible name of a secret method.Example
Technical Notes and Rationale
var
is repurposed to declare instance variables because its name is suggestive of "instance VARiable". Note that several early ES class proposals including the original ES4 proposals usedvar
for this purpose.static
instance variables andstatic
secret method definitions. But it is simplier to not have to explain how they differ from the non-static forms.var
declarations of a class collective and statically defined the "shape` of the secret instance state introduced by that class definition. Subclass can add additional secret state "shapes" but overall it is a step closer to objects with a fixed shape determined at class definition time and should also make it easier to lift the internal checks needed on instance var accesses.The text was updated successfully, but these errors were encountered: