-
Notifications
You must be signed in to change notification settings - Fork 25
Syntactic ideas to try to clarify the different execution time and scoping #33
Comments
imo using a slightly modified The only concern i have with curly braces is, what does |
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,
This works pretty well, though: |
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. |
Well, they do; that's what completion reform was about. But I agree that JS developers rarely see that (until But I think using the |
(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:
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:
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. 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. 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. |
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 |
Again, I think it's bizarre to say that we want to introduce to JavaScript a context where foo = foo evaluates the first |
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: 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 |
@neonsquare so the syntax (assuming class A {
static foo = 'hello';
bar := 'world';
} ? I think it's worth considering more full-fledged examples like these. I'd find the 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 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:
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 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 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?
as equivalent of
|
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:
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 😃 // implicitly generates getter
foo := 'bar';
// implicitly generates getter and setter
foo ::= 'bar'; |
@b-strauss |
@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. ;) |
@pluma Using a colon has been pretty thoroughly discussed and mostly ruled out because statically-typed dialects of JS almost unanimously use it because it's a pretty standard syntax to do so in many languages. class TheOne {
firstName: string = 'Thomas';
lastName: string = 'Anderson';
} They could decide to appropriate the colon for a different purpose but it would be pretty controversial IMO, particularly since many are still holding out hope that TC39 will add static typing to JS natively though that's unlikely at the moment. |
In an offline discussion @littledan reminded me of function default arguments, and how they already exhibit some of the non-obvious behaviors mentioned in the OP: function f({ bar: [baz] = qux }) { ... } in this example,
This convinced me that, with one small tweak, the current proposal's semantics can be used. The tweak is that the LHS and RHS should be evaluated in the same scope, i.e. the per-instance scope which (per other parts of this proposal) has access to This addresses the counterintuitive LHS/RHS split and puts (I still personally think it would be nice to make the syntax more "heavy", so that people realize that |
Evaluating the LHS per instance seems quite surprising to me, since property definitions seem like they are saying something about all instances of the class. They appear to be saying something declarative about instances. |
@domenic what about the inconsistency between: [foo] = 'bar';
[foo]() { return 'bar'; } In either case it seems like there's an inconsistency - it seems like we're deciding between whether it's more confusing to have the split be between properties and methods, or between LHS and RHS? |
I agree there's some asymmetry either way, but IMO this proposal has already whole-heartedly bought into breaking the symmetry between property declarations and methods, since property declarations put properties on the instance instead of on the prototype. |
Indeed, that's a strong point in favor of evaluating the LHS in instance scope. |
cc @wycats, who was the original advocate for computed field-names. I originally only had identifier field-names -- mostly out of min-viable-product -- but he had asked that we include computed as he had some uses for them in mind, I believe. |
@jeffmo I had also originally assumed only identifier-named properties, but I suppose it would be odd and incongruent to disallow symbol-named public property definitions. |
@zenparsing: Good point. To be clear, though: I wasn't necessarily suggesting omitting computed field-names -- only making sure @wycats could represent any upsides/downsides to evaluating LHS at instantiation time. I personally would find this odd, but I'd probably want to think about it more. I'm not sure I could do a great job of representing the position of someone who plans on using computed field names at the moment :) |
The biggest use case I want computed property names for is Symbols. |
@jayphelps out of curiosity, can you show an example? |
@zenparsing Contrived example: import { $$meta } from './symbols';
class Foo {
[$$meta] = {};
set bar(value) {
this[$$meta].bar = value;
}
} Other than that, computed property names for me are pretty rare. Even if we got private fields as spec'd I would still use Symbols because I often need "friends" to access them as well. |
FWIW, I think import { $$meta } from './symbols';
class Foo {
instance [$$meta] = {};
set bar(value) {
this[$$meta].bar = value;
}
} If it doesn't work as prefix, it might work as "group" class Foo {
instance {
[$$meta] = {}
foo = 'bar'
onClick = (e) => this.foo = e.detail.foo
}
} That being said, if this is just about avoiding @instance({
[$$meta]: {},
foo: 'bar',
onClick(e) {
this.foo = e.detail.foo;
}
})
class Foo {
constructor() { instance.setup(this); }
} Although, the implementation could have to do some work to grant non primitives clones per each instance, and a missed setup could be a disaster so ... maybe latter one is not a good idea. |
I want to draw our attention to @sebmarkbage 's https://github.com/sebmarkbage/ecmascript-scoped-constructor-arguments . I like this direction, and it completely changes the conversation about initialization expressions. It would allow initialization expressions to make use of constructor arguments intuitively, while remaining in class-body position. The translation of lexical capture to private fields would not be another way to express class-private instance variables. Rather, because it is in service of extending lexical capture intuitions, this internal use of private fields would be for instance-private instance variables. See the thread starting at tc39/proposal-private-fields#14 (comment) |
@erights I'm not sure I understand your comment there. AFAICT @sebmarkbage proposal wouldn't solve any of the problems mentioned here, like how to setup a There are also some smelly situations such this one which would confuse on how scope worked until now for the last 20 years in JS (if it's outer, it's available). I think is also quite redundant to write twice constructors arguments and yet that won't be a solution for properties definition at instance creation time. Am I missing something? |
@domenic Am I correct in understanding that according to the current proposal, this
should output That does seem reasonable imo; it makes sense that those properties would be defined when the class is defined. That said, if giving that up, and having both sides evaluated at class instantiation time is what's needed to keep this syntax, I suppose that's the least bad of all options. Again though, I'm not sure I see the problem, and I've been using this feature for some time now. cc @jeffmo |
PHP ;) does const x = '';
class Test {
const x = x;
public $x = x;
} Though PHP only allow assign compile-time constant and no computed property at all, so the problem is not very notable. |
In Python class attributes are also defined with class Foo:
some_list = [] Properties (with getters/setters) and class methods are just a special case of class attributes. However as these are class attributes, they are evaluated at definition time. EDIT: FWIW I have no more opinions on syntax because I recently exterminated my ego by using standard and then moving to prettier because it completely prevents me from having any opinions about formatting. |
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:
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:
I think this is particularly important in some of the common use cases, e.g. "method binding":
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:
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.
The text was updated successfully, but these errors were encountered: