js-classes-1.1
js-classes-1.1 copied to clipboard
My 2 cents...
With regard to considerations for TypeScript(TS)...
While I can empathize with this point of view, I have no sympathy for TS users. Nor do I believe they need any. Consider some 30 years ago when C++ was being developed as a macro pre-processor on top of a C compiler in much the same way as TS is "compiled" into JS. The development of the two languages has grown to the point where now C and C++ have long since crossed the line where a valid C program is not necessarily a valid C++ program. I believe the same thing will eventually be true for ES and TS. So I simply do not see any good reason for the development of ES syntax to be constrained by its potential effects on TS. This is not the first time one language has grown and forked from another, and I seriously doubt it will be the last.
With regard to var x;
in a class
...
"I puzzed and I puzzed 'till my puzzler was sore." Then I thought let
would be a much better fit than var
given the intended meaning. With let
it becomes perfectly clear that the surrounding {}
is the scope for the variable being defined. This is even more true when you consider that the contents of a class
definition is a prototype description. This means that, at least for me, the visual expectation was that the class
definition, being a non-function, would not be able to constrain the scope for a var
. With let
, however, all that's needed is a set of {}
to constrain the scope.
With regard to this->x
...
I find the potential to mistype =>
when one means ->
somewhat disturbing. The good news here is that on the occasion where =>
doesn't produce an immediate error, it will cause the code to do something obviously weird. That makes the problem somewhat easier to find than would be the case for a missing sigil from other proposals. Still, I think I like ::
better for this use. And yet there is still a problem.
Neither var
nor let
imply to the user that an object must be consulted to access the variables defined by that keyword. In fact, the natural implication of using those keywords is that within the scope, the corresponding variable can be reached simply by referencing it directly (var x;
/x+=2;
). To require a context object for accessing such a declaration is a definite break from the way they are currently used. For me, this falls back on why I cannot sympathize with TS.
With regard to hidden
...
First, hidden
would for me imply that whatever it tags is not enumerable, not that what it tags is not accessible from outside the class
scope. No matter how I look at it, the word for that is private
. But I understand the reasoning. I just don't buy that this is the correct term for the job. Second, the use of hidden
seems inconsistent. As I stated before, 'var
doesn't imply that the variable being defined has any instance connection. At least hidden
implies that something is doing the hiding, and thus there must be an object through which to access it. So even in this case, even though it feels like the wrong word, it also feels like it should be used consistently for things owned by an instance object but not accessible publicly.
Beyond these 4 issues, I like this proposal more than the class-fields proposal. At the same time, I would like to see some information on the expected implementation. The syntax seems to say most of the right things, but if that's not backed by the right implementation strategy, this proposal could potentially be even more bothersome than class-fields.
If one of the goals really is to have a complete proposal for class
that requires no future modification, well, you missed the mark, and not for the reasons above either, but it's a good start. I also like that the need for a static constructor/initializer wasn't forgotten. If I had to choose between going forward with the class-fields proposal, or this one, I would definitely prefer this one.
Thanks, @rdking, for your review.
In some ways, I think this would be really interesting, syntactically:
(Taken from an example on the private methods repo.)
class Counter extends HTMLElement {
const setX = (value) => { x = value; window.requestAnimationFrame(render); };
const render = () => this.textContent = x.toString();
const clicked = () => setX(x++);
let x = 0;
constructor() {
super();
this.onclick = clicked;
}
connectedCallback() {
render();
}
static {
window.customElements.define('num-counter', this);
}
}
(Note use of shorthand, let
and const
.)
@zenparsing I agree, this looks familiar enough that I understand what is going on without much thought. I know I'm having another conversation over at https://github.com/tc39/proposal-class-fields/issues/147 but really either one of these two approaches seem so much more like JS to me. If ->
let's me access the private scope of another object, grand as well. Simple enough and useful.
I find myself leaning toward ::
here, since it implies variable lookup within a specific (object) scope, rather than property name lookup.
@rdking's proposal has #obj
as well. I think any would suffice for that use case so whatever is decided, I'd be happy with.
@shannon Actually it was obj#
with the same meaning as obj->
or obj::
in this proposal.
I haven't given it much thought yet, but while this definitely solves the need for private data (presumably by making a closure around an instance), how does this leave room for selective sharing of the private variables with derived classes? As I've said before, it won't take long after the addition of private fields for there to be a need to share private data in a limited way. Even if it's going to be an additional proposal, it needs to be accounted for in any proposal that gets implemented
@rdking I commented for var
usage in other place I think you may already read, so I don't repeat it here again.
@zenparsing gave a interesting idea that, using const
and arrow function to replace "hidden method", which also solve some common cases like listener, and you don't need to worry about this
at all.
Actually, I also have a new idea close to it recent days.
One flaw of the shorthand syntax is, it only apply to instance variable, not hidden method, because you always need this->f()
to make it clear this
is the receiver.
But, hidden methods could almost only useful in its own class. Call it with other receiver just throw TypeError (though in theory, it doesn't do branding check so won't throw if not access instance var).
So why we need this
in this->f()
? So why not just make them bound to lexical this
like arrow function?
So mostly you are just using implicit this
, only in cases you deal with another class object, you use o->x
to denote the owner. And we could even consider reverse the order! Like x@o
which just eliminate the all possibility you may confused with o.x
.
Example:
import calcHash from 'util'
class X {
my x
constructor(x) {
x@this = x
}
my hash() {
return calcHash(x)
}
equal(other) {
return hash() === hash@other() && x === x@other
}
}
I just unified var
and hidden
to my
because my
may convey "only for my own usage" so you should never access my variables or call my methods (out of the class), and it's shorter. 😝
Any comment?
@rdking
Actually it was obj# with the same meaning as obj-> or obj:: in this proposal.
Yea sorry that's what I meant to type.
I agree that calling "private methods" with this
is mostly redundant.
Syntax-wise, I think we lost @
forever to decorators (which I think is OK).
Also, I worry that users would see a private "method-like" syntax:
class X {
my m() {}
}
and because it looks so similar to a regular method, they might be confused that they cannot access it with this.m()
.
The thing that I like about let
/const
is that it really is just the old closure class pattern.
In order to make the shorthands work we would have to do a bunch of work on of "Function Environment Records", but it's possible (at least from a spec point of view).
I'm sure it's been brought up before, but what if we make ::
a general "closure access operator". This is something that doesn't exist in the language at the moment and provides something relatively unique. Suppose someone writes something like:
var fn = (function() {
var x=1;
var retval = function Example() {
console.log(++x);
};
retval.doSomething(obj) {
//It's impossible to reach the x that belong to the closure of obj
}
return retval;
})();
var a = new fn();
fn.doSomething(a);
I'm thinking that doSomething
could reference x
via obj::x
. In the context of this 1.1 proposal, that would mean it makes perfect sense for @hax's example to be written like this:
import calcHash from 'util'
class X {
let x;
let hash = () => calcHash(x);
constructor(x) {
//Must use this::x instead of just x due to shadowing by fn arg.
this::x = x;
}
equal(other) {
return hash() === other::hash() && x === other::x;
}
}
Forgot to mention that this operator only works in a function on objects toting around a closure from the same function.
Here are some notes about how I think it could work.
The idea is to literally use the closure pattern. No WeakMap-like things behind the scenes.
Conceptually, in addition to keyed properties, all objects contain a possibly empty list of instance environment lexical environments, keyed by a symbol.
object | ||||
---|---|---|---|---|
properties
|
||||
environments
|
Each function object contains an internal slot named [[InstanceEnvironmentInfo]]
, which may be empty. If it is not empty, it contains a record with the following fields:
-
[[EnvironmentKey]]
: A unique symbol identifying the instance environment. -
[[Initializer]]
: A spec-internal function that initializes the instance environment. -
[[BoundNames]]
: A list of binding identifiers declared within the instance variable environment.
When an object is constructed, if the constructor has an [[InstanceEnvironmentInfo]]
it executes the initializer to obtain an instance environment. A pointer to this environment is then stored within the object using the specified key.
The abstract operations for function environment records are modified such that:
- Bindings are searched for in the declarative environment record first.
- If a binding is not found in the declarative environment record and the function has an
[[InstanceEnvironmentInfo]]
record and a[[ThisValue]]
object, then we attempt to look up the binding in the instance environment record of thethis
value, using[[EnvironmentKey]]
as the key.
Instance variable member expressions of the form x::y
are evaluated as follows:
- Search up the lexical environment chain looking for function environment records.
- If the function associated with the environment record has a non-empty
[[InstanceEnvironmentInfo]]
andy
is contained within[[BoundNames]]
, then use the[[EnvironmentKey]]
to search fory
in the correct instance environment record forx
.
@zenparsing
Syntax-wise, I think we lost
@
forever to decorators (which I think is OK).
Is there any syntax conflict with x@obj
and @deco
if we use x[no-newline]@obj
?
The thing that I like about
let
/const
is that it really is just the old closure class pattern.
Yeah, const x = (...args) => {...}
just work. But I feel it a little bit clumsy. I considered method(args) => {}
but found it's hard to see it's private, we'd better use a keyword here. I considered function method() {}
but it implies unbound semantic. So after all I chose my method() {}
, expect my
keyword could always means no this
for both variable and method for developers...
@hax Would you consider let method() {}
? It would have the same semantics as method() {}
inside a class
, but let
would scope it to the closure and not the prototype.
@zenparsing Wouldn't requiring a [[ThisValue]]
preclude access by static member functions?
@rdking Generally, for static methods I think you'd need the obj::x
syntax (which doesn't look at the [[ThisValue]]
)
@zenparsing Wouldn't that still affect the operations of the function environment record with regard to binding?
@rdking
Would you consider
let method() {}
?
I can't say let
could convey more or less lexical scope this
expectation than my
... and use let
implies mutable?
@hax it just occurred to me. If an instance of a class
operates within a closure, and we're able to assign private data within that closure, then we should also able to use function method() {}
within that closure as we would any other closure. To keep things consistent, these "private methods" would not be auto-scoped to the instance. That would limit flexibility fairly severely.
@zenparsing, do you think that's worth the cost of making variable scope even more complicated? To me it seems like "block scope vs function scope" is hard enough to teach without introducing a third kind of scope which is based on runtime values rather than syntax.
@bakkot The idea here is that an instance carries around a function scope that the member functions can all have access. It would basically have writing this:
class Example {
let private1 = "something";
function privateMethod() { //Do something here }
method1() {
console.log(private1);
this::privateMethod();
}
method2(other) {
other::privateMethod();
}
}
turn into something much like this:
var Example = (function() {
function Example() {
if (!new.target) { throw new TypeError(`Constructor Example requires 'new'`);
let private1 = "something";
function privateMethod() { //Do something here }
Object.setPrototypeOf(this, {
method1() {
console.log(private1);
privateMethod.bind(this)()
},
method2(other) {
privateMethod.bind(other)()
},
Object.getPrototypeOf(this);
});
//Back to your regularly scheduled construction
}
Example.prototype = {
constructor: Example,
method1() { throw new TypeError('Invalid scope access'); },
method2() { throw new TypeError('Invalid scope access'); }
};
return Example;
})();
There's no equivalent in ES6 for private variable access among siblings unless those siblings share a container holding their private data (like a WeakMap). The approach taken here is that a function having access to a closure from a given function can use the ::
operator to access a closure from the same function attached to another object. This way, siblings can peek into each other's closure.
@rdking It is not like that. a.method1
and b.method1
are the same function, but would have different variable resolution semantics. It really does require introducing a new kind of scope resolution which is based on runtime values rather than syntax. (Strictly speaking, I suppose, we already have that for this
. This is just a proposal to allow you to make arbitrary variables have the same delayed-binding semantics as this
. Though if I phrase it like that the idea makes me want to run screaming from the room.)
I am interested to hear @zenparsing's thoughts.
@bakkot I wasn't trying to present an exact equivalent, because there is none. You're right about the methods. I was only trying to show that the methods had access to the function's scope as if they had been created there.
Has variable scoping really been that hard to teach? I would've expected that something that's in C, C++, Java, PHP, & C# (and several other languages) would be fairly easy to teach.
@bakkot It's a bit off-kilter, but I like to explore ideas like that sometimes. I don't have a ton of confidence that I'd be able to convince anyone to implement these semantics. I'm also unsure about what an implementation would look like, although it seems like you can statically determine which bindings need to be looked up in the this
object, and avoid actually adding novel scopes. I'm wondering if the binding values themselves could be directly stored on the object instead of in a "real" closure.
Regarding mental model complexities, I think there are complexities one way or another regardless of what we do. I tend to be attracted to solutions that build on existing knowledge and patterns (like closure-for-private or "private symbols").
A large part of me would prefer that we do nothing (not even public fields) in order to minimize risk and future constraints.
@zenparsing yeah, I don't think the implementation would be hard, just teaching it.
Regarding mental model complexities, I think there are complexities one way or another regardless of what we do. I tend to be attracted to solutions that build on existing knowledge and patterns (like closure-for-private or "private symbols").
I think that's a very good design principle, but I also think it's very important that new bits of complexity not spill out into unrelated parts of the language. If you have to know that there's special scoping rules for variable references in class bodies, that spills out into everywhere you have to know about scoping rules. (Similarly, if you have to know that private symbols don't trigger proxy traps and don't show up in Reflect.ownKeys
, that spills out into everywhere you have to know about proxy traps or reflection on object keys. Though that cost is much lower than complicating scoping rules.)
These can easily be in tension - if your solution builds on an existing pattern by adding complexity to code which makes use of semantics related to that pattern, or by changing other parts of the language so that you can use code which looks like that pattern even though it needs special support to work, that will almost necessarily mean you're adding complexity to parts of the language unrelated to the problem you're actually trying to solve.
@bakkot Sure. All of that applies in spades to the current Stage 3 solution as well, though. I hope I was clear about that in my presentation regarding private symbols.
Also, I don't need a lecture about PL design ; )
@zenparsing Sorry, didn't mean to lecture! I'm just trying to sketch out where our meta-level disagreement seems to be.
(Object-level, I also disagree that whatever complexities there are in the current stage 3 solution spill out into the rest of the language. The fact that this is not the case (from my perspective) is one of the things I like most about it. I hope that was clear in my response to your presentation regarding private symbols!)
How do we get out of this zero-sum game where one side "wins" by refusing to acknowledge the other side's point of view?
@bakkot BTW, I think you bring a lot of PL talent to the committee and do wonderful work. 👍
@zenparsing I think a lot of this comes down to actual disagreements, not refusal to acknowledge points of view.
I don't know how to resolve disagreements other than talking for a long time, though. Having tried that, I don't really know what else to do. Some disagreements seem unavoidably zero-sum, like the reflection vs encapsulation debate.
@bakkot
To me it seems like "block scope vs function scope" is hard enough to teach without introducing a third kind of scope which is based on runtime values rather than syntax.
Actually, if consider the average JS programmers, I am sure classes 1.1 proposal is much easy to learn compare to current proposal. I already discussed the possible teaching issues with some educators in the China JS community. They all agree a proposal like classes 1.1 is much simple to learn for average JS programmers. The key point is, for average users, they need to learn not only syntax, but also semantic.
I do not have enough time now because we have a three days tech conferences from today, so I just throw the point, if you still not catch my point, I'd like to write a full explanation about it later.