gilbert / es-explicit-this

Explicit this naming in ECMAScript functions
18 stars 2 forks source link

explicit `this` and class constructor #5

Open hax opened 4 years ago

hax commented 4 years ago

TypeScript do not allow this parameter in constructor.

class Test {
  constructor(this: any) { // <-- Error: A constructor cannot have a 'this' parameter.ts(2681)
    //...
  }
}

It's very clear in derived class case:

class A extends B {
  constructor(this) { // <-- What `this` mean here??
    this // <-- throw ReferenceError
    super() // <-- `this` is only available after `super()`
  }
}

In base class case, even we can access this in the very beginning

class A {
  constructor() {
    console.log(this) // ok
  }
}
class B {
  constructor(x = this) { // <-- also ok, though TS report error: 
                          // 'this' cannot be referenced in constructor arguments.ts(2333)
  }
}

IMO we should also not allow explicit this in base class, because this in the constructor is never a parameter or argument passed by caller, but generated by the constructor itself.

Actually I feel we should make explicit this and constructor mutually exclusive, which means for

function f(this) {}

we should not allow new f() (throw TypeError) because in the new f() form, this is not an argument/parameter.

Essentially we should make f have no [[Construct]] internal method and no prototype property just like arrow functions, methods and accessors and all built-in non-constructor functions.

So explicit this will also provide a way to allow programmers clearly mark a function as a "method" without forcing them use method syntax.

tabatkins commented 4 years ago

I disagree, and would be confused by the restriction.

I accept that constructors have differences from methods here - there's no .call/.apply variant for new, and subclasses aren't allowed to access this before super(). But neither of those change how the this parameter acts once it's available; importantly, it still suffers from the exact same shadowing problems that motivate this entire proposal.

Whether you're declaring a class within another method, or declaring a this-using function within a constructor, you'd still benefit from being able to rename the constructor's this; in particular, the case of "define a class inside the constructor of another class" would, with the current restriction, have an unavoidable clash, requiring you to do the silly old let self = this; dance.

Overall this seems like an unnecessary semantic distinction that makes the syntax feel less unified, for no benefit.

hax commented 4 years ago

@tabatkins I understand your concern, but there will be more weirdness if we support explicit this in class constructor, for example, if we support constructor(this x) {}, it seems we should also support constructor(this {foo, bar}) {}, or even constructor(this {foo = 1, bar = 2}), but what the reasonable behavior should be?

tabatkins commented 4 years ago

I'm much more fine with disallowing that, since it's meaningless. (The value doesn't meaningfully have properties yet, or is in a TDZ.) Differences when things actually act different are great. ^_^

hax commented 4 years ago

@tabatkins Could you make clear about that ? Do you mean you prefer allowing rename of this argument of class constructor but disallowing destructure of this argument? Doesn't that also make syntax feel less unified?

since it's meaningless.

But neither of those change how the this parameter acts once it's available

@tabatkins I feel these two sentences contradict each other.

Strictly speaking, there is no this argument/parameter for class constructor at all ^_^ . this values in constructors are generated by the new mechanism of classes/base classes. So even we really want some syntax for renaming this in constructors, we'd better not use the syntax of argument/parameter. But to be honest, I don't find any other syntax alternatives better than current, especially current syntax of constructor(this) {} have already been implemented by TS and many other tools for many years.

ljharb commented 4 years ago

Sure there is - the new passes a new one, and if you invoke a non-class constructor as a function with .call or similar, it gets a different one.

tabatkins commented 4 years ago

Yeah, constructors have an implicit this argument, exactly like any other method, and so have the same arguments for renaming it as any other method does. The source of this doesn't matter; whether it's "generated by new" or passed in by the foo.bar() syntax or something more explicit, it's still a this argument and acts almost exactly the same. I think it will confuse people if renaming this did not work in this case.

The only difference from other methods is that there is a TDZ for this if the class is a subclass; in such cases there's no way to destructure the this. Since there's not really an argument for destructuring the this of a class constructor even when there is no TDZ, I think it's reasonable to restrict that functionality entirely in this case; I do not think people will be confused by destructuring this not working in this case.

(General "function called with new" cases should still allow the full set of renaming+destructuring; it's just class constructors specifically that we should separate out here.)

hax commented 4 years ago

It seems we have different understanding of what "this argument/parameter" mean.

My understanding is based on the current syntax and APIs of JS:

In all those cases, foo is the "this argument" passed to foo.bar.

On the other hand

In those cases, programmers can't pass "this argument" to foo.bar.

I am not sure how to express the impact to programmers mind of these two usage in more accurate words, but I truly believe it's very important to differentiate them in programmer's perspective.

hax commented 4 years ago

the new passes a new one, and if you invoke a non-class constructor as a function with .call or similar, it gets a different one.

@ljharb I don't understand how new could pass "this argument" to a function. And this issue is focused on class constructor, invoking .call on class constructor always throw. I changed the title to make it clear.

ljharb commented 4 years ago

The user using new isn't passing one, but the function itself is receiving one - this is the receiver, from the perspective of the function. How it's passed isn't conceptually material imo.

hax commented 4 years ago

@ljharb You are describing the mechanism, technically we can still name such this in new as "this argument" in spec, but I feel it's really confusing to name something not passed by the caller as "argument/parameter" in the programmer's perspective.

ljharb commented 4 years ago

Sure, but that's something this proposal is doing - it's not necessarily an already established definition. this as "receiver", however, is, as is looking at this as something that's defined for the function's perspective regardless of how it's generated.

hax commented 4 years ago

So it's more like a naming issue. Maybe we could make it clearer by renaming this proposal from "explicit this" to "explicit this argument" if it could help to solve the confusion we meet here.

The proposal text written by @gilbert 4 years ago never discuss about class constructor before my PR, also leave many details unspecified like whether supporting default parameters (created a new issue #8 ). And there were some other syntax alternatives which may do not have the implication from "argument"-like current syntax. Anyway, we are only in stage 0 now, as I understand this issue is more like a stage 2 block issue :)

Also, I see similar discussion of "this argument" in https://github.com/hax/proposal-function-this/issues/1#issuecomment-579123649 .

hax commented 4 years ago

I do not think people will be confused by destructuring this not working in this case.

@tabatkins As my experiences, it's dangerous to affirm people will not be confused in some specific way :) The community is too large with big variance. For example, React guys complains about forcing super() in class components. If it's a burden for some parts of the community to remember this specific differences between base classes and derived classes, then we could infer that they will probably be also confused by allowing destructuring in base classes but not derived classes.

We could also forbid destructuring totally in all class constructors, but some parts of the community may also be confused. Of coz we could have some faqs to explain that destructuring have TDZ issue in derived classes and also have no use case in base classes (I suppose), but it will introduce many extra criteria of the feature. For example, it also seem no much use cases of constructor(this) {}? Shall we also forbid it, and only support constructor(this renaming) {}?

Even we only support constructor(this renaming) {}, we also have an issue of future runtime features like parameter decorator, what's the runtime semantic of constructor(@deco this renaming) in derived classes? We may face similar TDZ issue like destructuring. Of coz we could forbid that case again or define some magic happen after super() (if introduce magic then such magic should also make destructuring work), but we would just increasingly introduce special rules. I feel it will also increase the complexity of the spec.

if renaming this did not work in this case.

Actually not only renaming, my proposing is disallow all explicit this syntax for class constructors.

this in constructors is very different than others:

With all these differences, I feel it's better to leave this in constructor untouched at all.


I have to admit there will be always some weirdness in all options, but I feel disallowing this in class constructors may be the simplest one, and we should notice that it is the only option which allow us change to other options in compatible way if we really want.

tabatkins commented 4 years ago

It is not passed by the caller, but generated by itself or its base classes.

This seems irrelevant to the argument; this proposal has nothing to do with how you initialize the this value for a function. Can you elaborate on why you think this distinction means people shouldn't rename the this value?

TDZ in derived classes.

Yes, that's a reason to disallow destructuring of this in class constructors. And I recognize that, due to the inconsistency it introduces, it does contribute to the argument to avoid touching this at all.

this value is an incomplete object in constructors which require specific attention when coding. For example, as typical OO theory you should avoid to call any virtual methods on this in constructors.

Irrelevant to the argument, as far as I can tell. You use this in the constructor all the time; whether or not it's "complete" has nothing to do with what you want to name it. Again, can you elaborate on why you think this distinction means people shouldn't rename the this value?

(If anything, this might be an argument that people are unlikely to try to destructure this in constructors, and thus we shouldn't worry about them being confused that it doesn't work...)

Constructors initialize the this instance and implicitly return this as result (though they could choose to dismiss this by returning another object), while normal methods/functions only use this as normal reference and "send messages" (use OO term) to it.

Also, as far as I can tell, irrelevant to the argument. Sorry to ask again, but can you elaborate on why you think this distinction means people shouldn't rename the this value?


Basically, my problem is that when someone is confused and asks "why can't I rename this to self in a constructor like I do with all my other methods?", answering with "because you can't destructure this in subclass constructors" is a non sequitur.

There are good reasons for cutting off technically-usable functionality in service of a simpler rule about the functionality (I've done it plenty of times in CSS, sometimes against significant opposition), but I don't believe this case justifies it. The line between usable and unusable functionality isn't complex, and I don't think it'll become more complex in the future, and the potential for confusion due to the missing functionality feels too high.

(For example, in CSS's calc() function we require whitespace around + and - operators, to reduce the chance of confusing parses. Technically, it's sometimes okay; calc(5- 3) would parse as a number, a "-", and a number just fine. But calc(5-3) parses as a number and a negative number (CSS doesn't have unary minus as an operator), and calc(5em- 3em) parses as one value with the unit "em-" and another with the unit "em". Allowing 5- 3 wasn't judged valuable to authors, so rather than allow it but also allow the possibility of those mistakes, we just required spaces in all cases. "+" doesn't even have as much confusion as "-", but it shares some of the confusing cases, and since + and - are intrinsically linked in people's minds, a blanket rule covering both of them was considered best. But we didn't require that for "" and "/", because they have no* parsing problems at all.)

hax commented 4 years ago

@tabatkins None of my comments say people shouldn't rename the this value in constructor. I'm sorry if I don't make it clear.

What I need to clarify is renaming this is only one part of the feature, it may be emphasis too much in original proposal text. I believe the most important sentence should be "its primary use case is to play nicely with the function bind proposal", as this sentence, class constructors are not the "primary use case".

Of coz we could adjust the goal of a proposal in current stages. I just want to be clear that many use cases of this proposal (like parameter annotation and decorator) do not relate to renaming.

I listed some differences of this in between constructors and methods to explain why we'd better rule out constructors totally, I'm not discussing renaming, but the whole feature. You could notice that in the original issue text, I never use renaming syntax.

hax commented 4 years ago

Current proposal proposed three syntax, f(this), f(this name), f(this {foo, bar}) (could also be any destructuring like f(this [foo, ...bar])), but we actually could explain the proposal as one sentence: Allow you treat implicit this parameter same as normal parameters, so you can:

(The only exception is providing default value, see #8 )

I think it's not so hard to explain to programmers why we don't support it in constructors: because constructors do not have "this parameter" (let me use "this parameter" which seems a little bit unambiguous than "this argument").

On the other side, it's not easy to explain/teach/infer the behavior for this in constructors if we support only some syntax combinations, you may need a table!

syntax base classes derived classes
annotate constructor(this: T) no? no?
decorate constructor(@deco this) yes? no? or yes (with some magic after super())
name constructor(this x) yes yes (with TDZ)
destructure constructor(this {foo, bar}) no? no? or yes (with some magic after super())

Note, though I don't want support default value initializer, it's possible to support it. But if we want both default value and constructors, we will need another rule, because there is no reason to support default value in constructors (this in constructors won't be undefined anyway).

hax commented 4 years ago

my problem is that when someone is confused and asks "why can't I rename this to self in a constructor like I do with all my other methods?",

With some clarification of the proposal name (renaming the proposal from "Syntax for Explicitly Naming this" to "Explicit this parameter") and related text, the answer could be:

A: Not only renaming, you can't use any explicit this parameter syntax in class constructors at all.

Q: So why we can't use them?

A:

Short version: Because constructors are not methods. ^_^

Long version: Because constructors do not have "this parameter", only methods have "this parameter".

Extra explanation:

Theoretically we could still support renaming this in constructor with same syntax, but it would be hard for the author of this proposal to define (and hard for programmers to learn/infer/remember) the behavior of different combination of features like parameter annotation/decorator/destructuring in different cases (base classes VS. derived classes). See the table in previous comment.

Note, while you can't use any explicit this parameter syntax in class constructors, at the same time, functions with explicit this parameter do not have [[Construct]], so invoking new f() will throw if f is a function with explicit this parameter. Explicit this parameter and new are mutually exclusive, because class constructors can only be invoked by new, so Explicit this parameter and class constructors are also mutually exclusive.

answering with "because you can't destructure this in subclass constructors" is a non sequitur.

Sorry, I didn't mean to use only destructuring issue as reason, just used it as a example of the consequences of supporting explicit this in constructors.

There are good reasons for cutting off technically-usable functionality in service of a simpler rule about the functionality

Agree.

but I don't believe this case justifies it.

Hope this comment could justifies it ^_^

ljharb commented 4 years ago

"methods" aren't really a thing; that's just a colloquialism for function-valued properties that tend to use their receiver, but can be "borrowed" and used with potentially any receiver.

hax commented 4 years ago

"methods" aren't really a thing; that's just a colloquialism

@ljharb

Yeah! I use "methods" because the original question use this word.

But there is another interesting thing, it seems "method" in spec means functions with specific syntax like class methods and let o = { method() {}, notMethod: function () { this } }. method() is a method even it doesn't expect this argument to be passed in, notMethod() is not a method even it does expect this argument. ^_^

So there is a gap between "method" in spec and the "method" in programmer's mind.

This problem bother me, I used to consider API name "Function.isMethodLike(f)" for the functionality of thisArgumentExpected, but obviously it was a bad idea. (Though I am not sure whether current name "thisArgumentExpected" is clear enough, we could discuss it in that repo)

ljharb commented 4 years ago

"Concise methods" still require a receiver, and can still be .called, so that distinction isn't particularly useful at this time.

hax commented 4 years ago

@ljharb I modified previous comment to make my meaning clearer :)

hax commented 4 years ago

Off-topic: About calc() in CSS.

@tabatkins

I happened to answer the question about the issue of +/- in calc() in Chinese CSS community 7 years ago (https://www.zhihu.com/question/21636985/answer/18841264). I remember that because some very senior programmers who can read spec well were also confused (it seems CSS grammar used to have a bug, miss the leading "+/-" in "num", which cause the confusion).

The problem of calc() in practice is calc(3em-1px) not work as programmer expect (while calc(3em*2) works). Theoretically calc() was a new syntax so it could introduce some contextual parsing rules for +/- to make calc(3em-1px) work as expect, though it may introduce other issues I don't know. The main consideration I guess is to keep the simpilicity of CSS grammar/parser.

If we can't make calc(3em+1px) work as expect then we'd better make the correct syntax simple, so I very appreciate your design of forcing spaces in both side and make calc(5- 1) as error! If allowing calc(5- 1), there will be room for tools using different policy, which do not have much benefit but only cause problems for the authors.

I would like also enforce spaces around * and / for simplicity and consistency in author's perspective, though I understand the reason of current design. And authors could use tools to enforce that if they want.


I think JS also have similar cases. Especially what constraints we should put in language and what constraints we could leave to tools.

In this specific issue, I chose to rule out class constructors totally, instead of including constructors but only allowing some special syntax combination. Actually I feel it's similar to your design decision of forcing spaces around both side and not allowing specific cases like calc(5- 1) ^_^

Of coz calc() case is much clear because you could always write calc(5 - 1) instead of calc(5- 1), in this case, we can not use "explicit this parameter" in constructors so programmers still need to write imperative code like const obj = thisor const obj = super() to create an alias for this in constructors if they really want.

tabatkins commented 4 years ago

I think it's not so hard to explain to programmers why we don't support it in constructors: because constructors do not have "this parameter"

You've said this more than once, and I don't understand what you could possibly mean by it. Constructors absolutely have a this parameter. Within the body of a constructor this is magically bound to a value, exactly like any other method.

Theoretically we could still support renaming this in constructor with same syntax, but it would be hard for the author of this proposal to define

Spec authors are the absolute lowest tier on the priority of constituencies; unless there's a very good reason, making a feature better for implementors, script authors, or webpage users absolutely wins over making a spec author happier. If you think you'll have trouble writing the details, you can ask for help.

(and hard for programmers to learn/infer/remember) the behavior of different combination of features like parameter annotation/decorator/destructuring in different cases (base classes VS. derived classes).

I'm not proposing any different combination of features for those cases. My proposal is that all class constructors allow renaming, and none of them allow destructuring or any other argument manipulation, like decorating. That's it.

I believe it's easy to understand the line being drawn - constructors can't manipulate their this argument, just rename it.

hax commented 4 years ago

I don't understand what you could possibly mean by it. Constructors absolutely have a this parameter.

I'm really confused. Why something never can be passed in could be treat as function parameter/argument? Maybe I understand the terminology of "function parameter/argument" in wrong way?

Within the body of a constructor this is magically bound to a value,

I understand it is "this value" as spec terminology, not "this parameter" or "this argument".

exactly like any other method.

Any other methods do not have such magic.

hax commented 4 years ago

it's easy to understand the line being drawn - constructors can't manipulate their this argument

I don't think it's easy, or at least not much easier than understanding the magic this of constructors vs any other methods.

For example, programmers will ask why we can't destructuring this in constructors of derived class? They may want to use properties of base cases.

tabatkins commented 4 years ago

I'm really confused. Why something never can be passed in could be treat as function parameter/argument? Maybe I understand the terminology of "function parameter/argument" in wrong way?

In:

class Foo {
  constructor() {
    console.log(this);
  }
  bar() {
    console.log(this);
  }
}
var x = new Foo();
x.bar();

The log will show the Foo object twice, because in both cases, this is "automatically passed in" (really: automatically bound in the context of the function body).

hax commented 4 years ago

@tabatkins

As I understand, the expression x.bar() pass the value of x as "this argument" with other arguments (in this example, no other arguments are provided) to bar method, which is basically same as (Foo.prototype.bar).call(x) or Reflect.apply(Foo.prototype.bar, x). I call it "this argument" because the spec and the docs use such name:

spec:

MDN:

As those API names, there are "this argument" and "the list of (normal) arguments" for a function call, you need to specify the values for both "this argument" and other normal arguments.

On the other side, when you invoke new Foo() (or Reflect.construct(Foo)), there is no way to specify a value as "this argument" to pass in, there is also no thisArg or thisArgument in the API signature of Reflect.construct(target, argumentsList[, newTarget]).

this is "automatically passed in"

As a non native English speaker, I'm not sure whether new Foo() case could be interpreted as "passed in". Maybe you mean when you invoke new Foo(), there is something generated then "passed in". But it's weird to me, with similar logic we could also say there is this "passed in" to () => this.

This is why I don't understand how this in class constructors could be called as "this argument/parameter" of that constructor. The terms I could accept are "this value", "this binding", "this reference".

Hope you can get me.


Anyway, if "this argument/parameter" is not a good term, is "receiver" a better term? Actually Java call it "receiver parameter". (And in Java, constructors also can't have "receiver parameter".)

Some details about Java "receiver parameter" Strictly speaking, the constructors of inner classes could have "receiver parameter", but it's actually a different thing and use different syntax (`Outer.this`). It's not `this` in constructors, but represent the instance of outer class which immediately encloses the instance of inner class being constructed (which is `this`).
hax commented 4 years ago

Spec authors are the absolute lowest tier on the priority of constituencies

@tabatkins

I really love the priority of constituencies and hope all our process of web standards include JS could follow the principles well... (omit some complains to some proposals :-)

What I really worry about is, in this case, if it's hard for us to define all possible syntax/semantic combination in a consistent way, it will also be very hard for programmers to learn/infer/remember the behavior of the feature, especially there are already too many FUD about this-related things.


I think we two have the agreement that class constructors are special. The divergence is how to deal with it.

I have given several reasons why I prefer that, and try to use a corresponding term ("explicit this parameter" or "receiver parameter" or something else we could agree) instead of overbroad term "explicit this" for that. (But even without this issue, I think we should also use a term follow the prior art of similar features of TypeScript/C#/Java.)

One of the important reason I like to repeat is, ruling out constructors is forward-compatible. We could introduce renaming this for class constructors or other feature relate to this for class constructors in future proposal after we have real world feedback of all related features. For example, we may still need to wait several years for parameter decorator. Currently I can't imagine any solid use case of parameter decorator for this in constructors, but I feel we are not the correct people to make such decision in current status. So I feel postpone the problems of this in constructors to the future proposal may be a better solution.

ljharb commented 4 years ago

I think the confusion is between arguments and parameters. A constructor has no this parameter; it has a this argument. (i think you could swap the definitions and it’d still make sense; i don’t know if there’s a universally understood definition for which term is which)

tabatkins commented 4 years ago

A constructor has no this parameter; it has a this argument.

Does the spec draw some distinction between arguments and parameters? As someone who's not deep in the technical weeds of the spec, I have no idea what that difference would be. I just know that both constructors and methods get something called this automatically bound in their bodies.

Maybe you mean when you invoke new Foo(), there is something generated then "passed in". But it's weird to me, with similar logic we could also say there is this "passed in" to () => this.

No, there's a big difference between a function closing over an existing variable that appears in its outer context (what you get with () => this), and a function generating a brand-new binding for a variable named this in its body (what both methods and constructors do).

On the other side, when you invoke new Foo() (or Reflect.construct(Foo)), there is no way to specify a value as "this argument" to pass in, there is also no thisArg or thisArgument in the API signature of Reflect.construct(target, argumentsList[, newTarget]).

Yeah, I think it's totally reasonable that there's no way to pass in a special value for this in a constructor; the whole point of a constructor is to generate a fresh version of the object.

But I repeated this question multiple times in my previous comment for a reason: can you elaborate on why you think this distinction means people shouldn't rename the this value?

All you're doing so far is pointing out that constructors and methods are different. I agree, they're different. But inside the function, they both automatically create a binding for a variable named this. Your proposal lets people rename that binding (among other things) in methods. Why do these differences mean people shouldn't be able to rename the binding in constructors?

ljharb commented 4 years ago

@tabatkins tbh i confuse myself often with this. the main point is, one thing is passed in, and the other is received. Whether a this is passed in or not, all non-arrow functions receive one.