Skip to content
This repository was archived by the owner on Jan 25, 2022. It is now read-only.

Which token is best for lexical declarations in a class declaration? #9

Closed
littledan opened this issue Jan 12, 2018 · 24 comments
Closed

Comments

@littledan
Copy link
Member

littledan commented Jan 12, 2018

As @ljharb suggested in #4 (comment) , we may want to use a token to set off lexically scoped function and variable declarations in class bodies.

class C {
    <placeholder> function x(v) { return v+y }
    <placeholder> let y = 1;
    method() { return x(1); }
}
new C().method();  // => 2

What would be a good value for <placeholder>? We could use basically whatever we want here, as long as we're ok with a no-newline restriction between the token and the following lexical declaration. Some candidates:

What do you think about these? In #4, some disadvantages of static and class have been mentioned. As an initial strawperson, what if we go with inner?

@ljharb
Copy link
Member

ljharb commented Jan 12, 2018

inner could get confusing with nested class definitions.

@jridgewell
Copy link
Member

lexical is very technical. Maybe scope or scoped?

@littledan
Copy link
Member Author

I like scoped!

@ljharb
Copy link
Member

ljharb commented Jan 12, 2018

Is scope less technical than lexical?

@bakkot
Copy link

bakkot commented Jan 12, 2018

@ljharb, what's the confusion with inner and nested class declarations?

@jridgewell
Copy link
Member

Is scope less technical than lexical?

I can still barely tell you what it means. I’ve at least heard of function scope, though, and curly-brace scope with let/const.

@domenic
Copy link
Member

domenic commented Jan 12, 2018

nested

@ljharb
Copy link
Member

ljharb commented Jan 12, 2018

@bakkot basically, which inner is it referring to? Indeed it’d be lexical, so it’d be everything that’s nested lexically inside the class (ᕕ( ᐛ )ᕗ) but I’m not sure how clear that word makes it.

littledan added a commit that referenced this issue Jan 15, 2018
This patch specifies lexically scoped declarations in class bodies,
with a token preceding them. Because we haven't figured out which
token to use (#9), <placeholder> is used instead. Some main design points:
- let, const, class and function (including async fns, generators) are
  supported, and var declarations are not supported.
- Function declarations are hoisted to the top of the block, before
  evaluating the extends clause.
- let, const and class declarations are evaluated interspersed with
  static public field initializers.
- The class is visible with a binding initialized when the lexical
  declarations are evaluated.
- Unlike static public field initializers which are in a scope analogous
  to a method (with super property access, and this being the class),
  the scope of lexical declarations inherits directly from the outer
  scope, similarly to computed proerty names and the extends clause,
  which implies:
    - this, super, new.target are inherited from the lexical context,
      and are not related to the current class definition.
    - arguments is not poisoned and is inherited from an outer definition
    - yield, await can be used if possible in the enclosing function.
littledan added a commit that referenced this issue Jan 16, 2018
This patch specifies lexically scoped declarations in class bodies,
with a token preceding them. Because we haven't figured out which
token to use (#9), <placeholder> is used instead. Some main design points:
- let, const, class and function (including async fns, generators) are
  supported, and var declarations are not supported.
- Function declarations are hoisted to the top of the block, before
  evaluating the extends clause.
- let, const and class declarations are evaluated interspersed with
  static public field initializers.
- The class is visible with a binding initialized when the lexical
  declarations are evaluated.
- Unlike static public field initializers which are in a scope analogous
  to a method (with super property access, and this being the class),
  the scope of lexical declarations inherits directly from the outer
  scope, similarly to computed proerty names and the extends clause,
  which implies:
    - this, super, new.target are inherited from the lexical context,
      and are not related to the current class definition.
    - arguments is not poisoned and is inherited from an outer definition
    - yield, await can be used if possible in the enclosing function.
@littledan
Copy link
Member Author

I'd like to choose a token to be a concrete strawman (cf #14). How about local?

@jridgewell
Copy link
Member

local is fine with me, we can bikeshed further in the meeting.

@littledan
Copy link
Member Author

Please keep bikeshedding here too everyone!

littledan added a commit that referenced this issue Jan 17, 2018
Strawperson adopted pending future discussion in #9

Closes #14
@allenwb
Copy link
Member

allenwb commented Jan 22, 2018

I still prefer no leading token. With class as an acceptable alternative.

People, presumably lean about blocks and lexical scope early in the process of learning modern JS. These proposed class body lexical declarations have a meaning that is already consistent with what they have already leaned -- they are scoped to the enclosing { } scope, which just happens to be a class body.

Once they learn that class bodies allow lexical declarations (distinct from fields) the prefix token is just noise.

@littledan
Copy link
Member Author

@allenwb We've heard people express reservations about both no leading token and class in threads in this repository. Are there any others that seem reasonable to you?

@allenwb
Copy link
Member

allenwb commented Jan 23, 2018

Well, and I've expressed reservations about having such a token. 😀 I'd suggest getting input from people that may have strong options about consistency of lexical declarations and scopes. Maybe: Mark Miller, Dave Herman, Brendan, Sam T-H, etc.

I think static would be very bad and it would just reopen the confusion WRT static properties. class is plausible but the other all are just arbitrary noise words that don't provide any hint at why they are needed in this specific context and not other nest scopes.

I would be content with only having function declarations with no prefix.

@erights
Copy link

erights commented Jan 23, 2018

Thanks @allenwb . My ranking of choices:

  • Per-instance declarations get a keyword prefix, lexical declarations get another. Since I don't expect to get agreement on the first part, I'll avoid enumerating possible keyword choices for this one.
  • Per-instance declarations get a keyword prefix, lexical declarations get none. (Or rather, no extra keywords beyond those they already have.) Again, I don't expect to get agreement on the first part, so I'll avoid enumerating possible keywords for the per-instance declarations.
  • nested
  • lexical
  • local
  • shared

Choices I consider disqualified:

  • static
  • #
  • inner
  • Offering lexical declarations only to functions.
  • class

The nested vs inner contrast is based on the Java meaning of "nested class" vs "inner class". An inner class is within the scope of an instance of the parent class. A nested class is within the scope of the parent class. With nested, a nested class Foo ... in JS would happen to mean exactly what a good Java programmer would expect.

I initially liked class. But class class Foo ... would be a terrible way to declare a nested class.

@allenwb
Copy link
Member

allenwb commented Jan 25, 2018

I initially liked class. But class class Foo ... would be a terrible way to declare a nested class.

I would think that if we allowed nested class definition we could make it a special case that doesn't have the repeated class prefix (after all, it's really just a noise word).

However, I'm not sure that nested classes can work because of staging issues WRT evaluation order of class member elements. Would need to work out details to be sure.

@allenwb
Copy link
Member

allenwb commented Jan 25, 2018

Parts of a relevant conversation with @getify:

Getify:

i'm well aware of the various competing interests at play.

but as a teacher and advocate of JS, by far the things people struggle with and "hate" about JS are inconsistencies.

so i stand by my assertion that when consistency is considered unimportant or "uncompelling", the result is often a thing that's harder to teach/learn.

@allenwb

So, strictly focusing on the syntax and not the pros/cons/motivation for the functionality, from a consistency perspective what do you think of:

class C {
local function f() {}
m() { f() }
}

vs

class C {
function f() {}
m() { f() }
}

as the syntax for declaring a normal lexically scoped function (i.e. not a method) whose scope is a class body.

@getify:

so my initial reaction is just function(){} not local
since there's already precedent for function declarations inside "blocks"... which is not what the class { } is, but it sorta looks like it if you squint

will these local functions inside class bodies hoist?

@allenwb

yes, everything woks just like any other lexical scope

@getify

if they hoist, then i think function f(){} declaration style is the best

the difference between conside class methods and full function declarations should be enough of a visual clue

my experience with lexical scoping and teaching has been, people expect for { } to create a block they can scope things to

and they're used to function declarations doing that (although block-scoped functions has been dicey), and they're now used to the idea of let/const in blocks

the { } around a class body NOT being a lexical scope has been a strange/surprising inconsistency in that respect

another exception they have to mentally juggle

so moving class bodies to be more like lexical scopes will probably help with that

OTOH, another super confusing thing to a lot of learners has been whether this contexts and lexical scopes mix

@zenparsing
Copy link
Member

@allenwb Ah, reminds me of Paris in the spring... (Those @'s sure were beautiful in their youth!)

There's a minor technical issue with using bare function x() {}: async methods. The parser can see:

class X {
  async function <REST OF PROGRAM>

I suppose we'll need a couple of tokens of lookahead to disambiguate async methods from an async function declaration. Is that a problem?

@getify
Copy link

getify commented Jan 25, 2018

@zenparsing is it not just async function foo( .. vs async foo( ..?

@allenwb
Copy link
Member

allenwb commented Jan 25, 2018

is it not just async function foo( .. vs async foo( ..?

I brought this up long ago and suggest we ban unquote function as an async method name. @erights (I believe) pointed out that it is resolvable via lookahead so the restriction wasn't made.

@erights
Copy link

erights commented Jan 25, 2018

@allenwb @getify I agree about consistency. However, it seems to me that both generality and consistency argue that we do not make a special case for lexical function declarations, as opposed to lexical declarations in general --- including let, const, class, function*, async function, and async function*. My point is not to enumerate the lexical declarations that should be included, but rather to say that all lexical declarations should be included in the same way.

@allenwb I am curious if there is a lurking need to treat nested classes specially. I understand the issue --- that classes have initialization phases, including evaluating expressions (superclass and computed properties). But I don't see any reason why this should cause nested classes to need special handling. If nested classes do not otherwise need to be a special case, then we should not make a special syntactic case for them merely to avoid the text class class Foo .... Rather, we should avoid the token class as the indicator of a nested declaration.

@allenwb
Copy link
Member

allenwb commented Jan 25, 2018

@erights I'm find with (for consistency) including all of those without a special token. But if the choice was only function without a special token or all of them with a special token, I'd go with the only function option. Basically if class bodies are super special then we are justified in only allowing function (note in all proposals we seem to be excluding var). If class bodies aren't so special (lexically) then there is not reason not to include all the lexical declarators without using a special prefix.

Regarding nest classes. I'm concern about things like:

class C {
   class N extends C { /* stuff that depends upon C being following defined */}
}

But I guess it isn't any worse than:

class C {
   constructor() {
      /* stuff that expects C to be fully defined */
   }
   static x = new C
}

In either cases the code will probably error out at runtime.

@littledan
Copy link
Member Author

At the January 2018 TC39 meeting, @waldemarhorwat expressed strong skepticism for including any sort of token here. From Waldemar's point of view, I believe, a token would be meaningless noise and add something additional to remember. On the other hand, many others have expressed concern that bare function declarations in class bodies would be confusing. I'm not sure how to find a good, widely agreeable solution here.

@littledan
Copy link
Member Author

The current Stage 3 proposal includes only static class features, and no lexical declarations.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants