proposal-class-public-fields
proposal-class-public-fields copied to clipboard
Syntactic ideas to try to clarify the different execution time and scoping
At the March TC39 meeting, I and others were able to articulate our misgivings about the current proposal. The obviously troublesome cases are things like:
class C {
[this.bar] = this.bar;
[baz] = baz;
}
where the left-hand side executes at a completely different time, and in a completely different scope, than the right-hand side. Myself in particular find the idea of two sides of an =
sign having different bindings to be just too strange.
I think this could be helped with syntactic work to make it clear that the right-hand side is a "thunk" executing later and in a different scope. Here are some strawmen ideas:
[baz] := baz // at least makes it clear this is not straightforward =
[baz] := { return baz; } // very clear this is a thunk
// or allow both, i.e. use arrow function body-esque rules?
[baz] := do { baz; } // use do expressions as the thunk (allows omitting return)
[baz] do= { baz; } // !?!?
// or any of the above using different sigils, e.g.:
[baz] <- { return baz; }
[baz] <- { baz; }
I think this is particularly important in some of the common use cases, e.g. "method binding":
class C {
method = (foo) => bar;
}
I have seen people do this just because they like arrow functions and want the concise body, not realizing that this is a significant semantic shift: removing the prototype property, and creating a new function instance every time a C
is constructed. People's mental model, in other words, is that this is only evaluated once.
Contrast:
class C {
method := { return (foo) => bar; }
}
Here it should be intuitively clear from looking at the code that there's a bit of code that will run, creating and returning a new arrow function every time. That's hugely beneficial.
imo using a slightly modified =
sigil that optionally also requires curly braces around the initialization expression seems totally legitimate to me.
The only concern i have with curly braces is, what does method := { a: b }
do? I'd rather not see us carry forward the mistake that was made with arrow functions.
Yeah, perhaps making the curlies optional is not a good idea.
For the record: I'm not in complete agreement with the problem statement, but I am totally willing to explore and consider alternate syntax if it fits the same simplicity mold.
The only strawpeople I have concerns with are the ones with curlies -- namely that curlies are already overloaded in JS to represent both an object or a block. We're sort of after a block-like thing here, except that blocks are statement lists and not expressions (and we're specifically looking for an expression container).
So, of the proposed options you stated, baz := this.baz;
or baz <- this.baz;
seem like the best contenders.
:=
is viable at a technical level, but with consideration of typecheckers like Flow/TypeScript/CloserCompiler it could get a little confusing for type annotation extensions:
baz: number := 42
(a bit noisy with the :
)
Alternatively: baz: number = 42
(but we're back to the original concerns)
This works pretty well, though: baz: number <- 42
We're sort of after a block-like thing here, except that blocks are statement lists and not expressions (and we're specifically looking for an expression container).
Can you explain why? The difference is minimal at best, given comma expressions.
We're looking for an expression to be evaluated to a value (and eventually assigned). Statement lists don't evaluate to a value, though.
If we had do-expressions, this would be a good space for them as you suggested (and I would love to have do-expressions for lots of reasons) but as of the last time they were discussed they were pretty contentious -- so I wouldn't want to block on that contention here.
Statement lists don't evaluate to a value, though.
Well, they do; that's what completion reform was about. But I agree that JS developers rarely see that (until do
expressions advance), since eval
usage is rare.
But I think using the return
keyword, as in function bodies, certainly makes it explicit.
(PS: cc @zenparsing)
@jeffmo asked me to write my experience with this on twitter:
I have been naively using this feature for the past 6 months, mostly in React classes. I used it for:
- method definitions using arrow functions for event handlers (so I don't have to bind them in the
render
method) - static
propTypes
andcontextTypes
- the occasional instance state
I kinda knew that it is deferred execution (because how would it work otherwise ) but used it without thinking much about it and haven't ran into any problems. I also just did a scan over the codebase and a relevant piece of data is that the RHS is always pure expressions in my case, so that might explain how I didn't run into any wtfs using it.
My 2 cents:
method := { return (foo) => bar; }
defeats most of the reason people use arrow functions in fields, terseness of autobind; now it's rather verbose compared to alternatives. As you mention, people sometimes don't realize using them creates a new instance of that function for each class instance, so in reality I'm not convinced it's something to be encouraged as a go-to since that could create unintuitive bugs for people, foo1.method !== foo2.method
. I don't believe you're encouraging it per se, but just clarifying for drive-by readers.
I feel like the existing RHS behavior is a very normal behavior of class languages that most would realize/recognize if it were anything but an arrow function. e.g. foo = new Foo();
I think the discussion is a valid one to have, but given the current suggestions I would still land on the original proposed syntax.
I'm curious about why the LHS is evaluated separately and not along with the RHS? To me, that's the least intuitive in discussion. If that becomes a sticking point I'd like to see TC39 punt on computed properties for v1 as I had heard was the case.
I personally don't like the <- idea because I see to much clashing with things like generic syntax, JSX, "less-than-minus-X" a.s.o. I've to say though, that I do not really see a big problem with the status quo too.
The syntax - combined with arrow functions can always looks a bit strange IMO.
class Foo {
method = x=>x
}
class Foo {
method <- x=>x
}
class Foo {
method := x=>x
}
Ok thats picking out this particular arrow function use case - but this seems to get common for event handlers because it solves the this binding issue:
class View {
onClick = (ev)=> this.setState(...)
}
I proposed using a prefix - but I agree that it adds more boilerplate and doesn't make it look less like an assignment:
class Foo {
instance onClick = (ev)=>this.setState()
}
it was just meant to stay in line to:
class Foo {
static method = x=>x
}
I think it would be quite clear what it does though. "Set instance's onClick to...".
Using "thunks"
As outlined may be an option, but it also looks quite heavy to me. This whole thing is meant as syntactic sugar to make constructors less necessary. But if it adds even more fanfare than why bother at all?
class A {
a = 42;
b = "Hallo";
c = 3.14;
d = 3/4;
}
class A {
a := { return 42; };
b := { return "Hallo" };
c := { return 3.14; };
d := { return 3/4 };
}
class A {
constructor () {
this.a = 42;
this.b = "Hallo";
this.c = 3.14;
this.d = 3/4;
}
}
errhh... no I don't think those thunks are really a big improvement.... ;)
Crazy idea:
class A {
this {
a = 42;
b = "Hallo";
c = 3.14;
c = 3/4;
}
}
???
I still think that the status quo is not that bad if such a feature is wanted...
@jayphelps computed method properties are evaluated at class definition time - it would not be consistent to ever evaluate computed instance or static properties at any other time imo.
@ljharb I can absolutely see that argument, though fields are different as described in that their value is set at instantiation vs. methods at definition, so they're already inconsistent by necessity (obviously). I don't feel super strongly about it, just thought I'd mention what felt intuitive to me.
though fields are different as described in that their value is set at instantiation vs. methods at definition
That is precisely what this thread is trying to solve: make that inconsistency clear, by delimiting the code that runs at a different time in some outstanding way.
@domenic to me the equals sign by its nature inside of a class body denotes that already, but admittedly that's almost certainly because that's just how all the languages with classes I can recall do it. Is there any lang you're aware of that doesn't have this behavior?
@jayphelps all languages with classes that I've worked in (C++, C#, Java, JavaScript) do not have a syntax which uses =
for initializers.
Again, I think it's bizarre to say that we want to introduce to JavaScript a context where
foo = foo
evaluates the first foo
at a different time (and a different number of times) than the second foo
.
I'm with @jayphelps here - to me the context of the class definition was enough to make this field initialization intuitively understandable. This whole class definition thing is one big pile of syntactic sugar anyways... ;)
@domenic: Well statements like v = v + 1 actually seemed quite bizarre to me for quite a long time for something that is really just: (setf v (+ v 1)) - but I'm comfortable with the thought that a lot of people adapted to = meaning something very different (an action) than equality (a relation).
Don't you think the thunk syntax would destroy the whole purpose of this syntax sugaring?
Don't you think the thunk syntax would destroy the whole purpose of this syntax sugaring?
I don't think so, no. It would make people more aware of the weight of what they're doing (causing code to be run and objects created on every initialization), which as I pointed out in my OP is pretty important for common cases like arrow functions. And it would clarify the scoping and runtime rules. A few extra characters to make the syntax sugaring, as you say, match the actual programming model, seems like a worthwhile investment.
Just a naive question: shouldn't you also take static properties into account in this discussion? Or are they intended to have different syntax because the assignment happens at different times?
@pluma Values of static properties are only evaluated once at class definition time - so both of the criticized aspects do not apply.
@neonsquare so the syntax (assuming :=
which @domenic proposed) would be
class A {
static foo = 'hello';
bar := 'world';
}
?
I think it's worth considering more full-fledged examples like these. I'd find the :=
syntax (as well as the <-
syntax) quite jarring -- unless =
without the static
would be added as a way to assign to the prototype (which would break compatibility with current implementations of this proposal but allow replicating the behaviour of the function-with-prototype pattern).
It's important to avoid confusion, but it's also important not to fall into traps like PHP does where every new feature gets a new operator (leading to nonsense like having to use backslashes for namespaces because all the alternatives were already taken).
@pluma No @domenic 's proposed syntax would be:
class A {
static foo = 'Hello';
bar := { return 'world'; }
}
and perhaps variants like this when do expressions are there:
class A {
static foo = 'Hello';
bar := do { 'world'; }
}
I understand the reasoning behind making this "instance field initializers" look more heavy to remind people about the perceived computional complexity of them being evaluated on any instance creation. I can't help thinking about how they somehow look wrong to me. On the other side - I don't think that it is always necessary to cater for any misunderstanding a newbie might have. This is an "assignment within a class declaration" - that was enough speciality for me to assume special evaluation rules anyway. If there really is an argument about documenting whats going on - perhaps something like this would be more enlightening:
class A {
prototype.foo = 'Hello';
on new { this.bar = 'world'; }
}
Ok after much discussion the final syntax is:
class Foo {
bar ¯\_(ツ)_/¯ : 104
}
This syntax doesn't look like assignment at all, is enough fanfare to warn users about what they may do and should not collide with other features!
;-)
I've been using the proposal for a couple months in my job, and I haven't seen any code in the wild that uses computed class fields, nor do we have any in our codebase. I had read about the field being evaluated at a different time, but hadn't paid it much attention. Most of my usage is with static props.
I apologize if this is an inappropriate question for the thread, but why not evaluate both sides at the same time? If you used something like this
from the class body, it'd basically basically just be shifting stuff up one indentation level from the constructor.
class Foo {
static abc = 'abc';
this[Foo.abc] = true;
this.foo = false;
this.bar = (e) => console.log(e);
}
is sugar for
class Foo {
constructor () {
this[Foo.abc] = true;
this.foo = false;
this.bar = (e) => console.log(e);
}
}
Foo.abc = 'abc'
Although I could also see how someone might find that confusing as well.
Frankly the entire point of having syntax for class fields is to be able to declare instance attributes and to avoid having to define constructors (with the entire this
and super
song and dance) to assign initial values to them. Forcing each assignment to happen inside yet another block-like construct kinda defeats the purpose.
If you make developers wrap each individual assignment in a block, nobody will use this feature.
What about less magic? Why not call them initializers and make them take functions?
class Foo {
foo: () => 'hello'
bar: initBar
}
as equivalent of
class Foo {
constructor(...args) {
super(...args);
this.foo = (() => 'hello').call(this);
this.bar = initBar.call(this);
}
}
If you make developers wrap each individual assignment in a block, nobody will use this feature.
To some degree that is indeed the purpose. Users should not use this feature... without knowing that it may do more than they think (evaluation at each instantiation).
My personal opinion: If it would be only shortening of syntax and if the syntax would be that heavy, I may not really use it because writing a constructor is often shorter if there are multiple fields to initialize. Think of things like:
class Foo {
constructor(a,b,c) {
Object.assign(this,{a,b,c});
}
}
But: Special Syntax like those field initializers is always an option for static analytics. It is a good place to annotate static type information (see Flow and TypeScript). Its restricted syntactical structure makes static analysis easier than with arbitrary initialization code.
So: a Special syntax has more purpose than just making the code shorter. Still: To me the originally proposed syntax is the best option so far. I personally do not see the instance evaluation time as a convincing reason to use heavy block syntax here. Maybe better variants are possible.
Although I read the whole issue I don't really understand the premise. Is the sole reason of this proposal to highlight to the developer that this is not their everyday variable binding?
Of all the strawmen ideas I prefer:
foo := 'bar';
Everything else seems a bit too verbose to me.
On a side note; Now that I see it I would love to have some sugar for implicit getters/setters in the style of Newspeak :smiley:
// implicitly generates getter
foo := 'bar';
// implicitly generates getter and setter
foo ::= 'bar';
@b-strauss get foo() { return 'bar'; }
- there's no need to use properties for that. Also, where would an implicit setter store its value? Let's not derail the thread with an unrelated proposal :-)
@ljharb Well if I had the syntax I would use it. Just because it's less to type :). Where the values would be stored would be an implementation detail. Everything would go through the accessors, like Dart does it. But you are right, let's not continue that discussion here. ;)