Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grammar, Nodes: Class fields/class property initializers #4552

Open
xixixao opened this issue May 16, 2017 · 24 comments
Open

Grammar, Nodes: Class fields/class property initializers #4552

xixixao opened this issue May 16, 2017 · 24 comments

Comments

@xixixao
Copy link
Contributor

xixixao commented May 16, 2017

We should be able to write the equivalent of

class C {
  b = 3;
  c = () => 4;
}

Can it be

class C
  b: 3
  c: () => 4;

or does the b property have different semantics?

@GeoffreyBooth
Copy link
Collaborator

Is this the same as class properties, that are only in stage 2? See #4497 (comment)

@connec
Copy link
Collaborator

connec commented May 19, 2017

Bit of a brain dump incoming 😅

I did some digging into the current status of the public fields proposal. From what I can tell, this proposal to standardise orthogonal syntax for class properties is the bleeding edge for the standards work. The meeting notes I found this in are labelled for ES8, so there doesn't seem to be any concrete end in sight for standardising class properties due to various critiques about the semantics and syntax, and the dependencies between it and other proposals (e.g. private fields).

In general, there seems to be a bit of stagnation in the standards around classes, and there's a whole other discussion about how tools such as babel make that work harder by providing early access to non-standard features like class properties, which people then conflate with standard ES features.

There's no clear way for us to move forward here imo. We either:

  • Throw our lot in with babel and others and support their syntax. This will probably mean sacrificing executable class bodies, and would require us to update the syntax if the standard changes (if it's ever standard).

  • Become early adopters of the orthogonal class syntax (which isn't even staged yet, afaict). This would probably allow us to continue using ECBs if we were happy to make own a reserved word. We'd have to figure out an alternative sigil from # for private fields. We'd also have to update if the standard changes (again, if it's ever standard).

  • Figure out a syntax and semantics that makes sense for CoffeeScript. This is (clearly) my preference, but it conflicts with the current goal of CS2 of greater alignment between CS and ES. The benefits are that we could consider each change to the ES spec separately and decide whether it's something we want CS to support, and how it should support it. The downside is people don't get the whitespace sensitive ES they're hoping for, and instead have to learn a parallel syntax.

  • Continue to hold out for a standard to solidify. This is another robust option, but will undoubtedly cause some adoption resistance and issues asking for support. At some point people will ask why if it's good enough for babel, it's not good enough for CS.

As a bit of an aside, I think the main reasons to care about this feature at all are:

  • Improved static analysis. It's much easier to extract fields etc. from a class syntax than scanning a constructor. I think this is a legitimate issue, and more declarative syntax often enables a lot of useful tooling that would otherwise be impractical.

  • Bound methods. This is probably the big one for users, however since it's literally working against JS' type system (such as it is), a syntax solution to this is almost always going to have a bunch of caveats (for example babel's interpretation disallows super in public function properties, as they're strictly not 'methods' in the general sense). I think a syntax for bound method access would be a better answer to this problem, as it doesn't attempt to hide any magic.

Let me know if I'm missing any!


Separately, If anyone comes across this who has anymore information on how public properties is progressing on the standards track (or anywhere besides mailing lists and ESDiscuss to look for info), it'd be good to hear about it!

@farwayer
Copy link

farwayer commented Jun 17, 2017

I was really surprised class method binding is not available in CS2. It always was one of cs killer feature. I tried make it thru class properties and found out class properties is not working in CS2. It is not ES standard yet of course but look like will be.
So for now I can make simple binding in JS (via class properties with babel) but can't do it in CS2. It is very uncomfortable.

@connec
Copy link
Collaborator

connec commented Jun 17, 2017

It seems some decisions were made at the latest es8 meeting - in particular the own keyword has been dropped which removes a useful disambiguation for us 😞

@GeoffreyBooth
Copy link
Collaborator

GeoffreyBooth commented Jun 18, 2017

We can add support, but the compilation output would need to be Stage 4 ES (i.e. ES2017 or below). In other words, our version would need to resemble the Babel plugin, that converts the initializers syntax to ES5. And if or when the feature is standardized, our output could be updated to output ES2018 or whenever it lands, rather than the converted ES5 or ES2017. This is similar to the object destructuring being added in #4493.

@GeoffreyBooth GeoffreyBooth changed the title [CS2] Support property initializers [CS2] Proposal: Class property initializers Jul 30, 2017
@GeoffreyBooth GeoffreyBooth changed the title [CS2] Proposal: Class property initializers Proposal: Class property initializers Sep 8, 2017
@GeoffreyBooth GeoffreyBooth changed the title Proposal: Class property initializers Proposal: Class fields/class property initializers Sep 27, 2017
@GeoffreyBooth GeoffreyBooth changed the title Proposal: Class fields/class property initializers [Awaiting Stage 4] Class fields/class property initializers Dec 29, 2017
@GeoffreyBooth
Copy link
Collaborator

Here’s a good overview of current ES proposals. Some CoffeeScript-inspired ones on there. It covers the class-related ones that are in progress, including how they’ve been changing from stage to stage.

@microdou
Copy link

I absolutely love this new feature and cannot wait for its implementation in CS. It looks like it's going to be at Stage 4 very soon. tc39/proposal-private-methods@7aa58d7

Note that there are both public & private fields, not mentioned in original post. The private field name is preceded by #, which likely is not compatible with CoffeeScript's annotation.

class Counter {
  // public field
  text = ‘Counter’;

  // private field
  #state = {
    count: 0,
  };

  // private method
  #handleClick() {
    this.#state.count++;
  }

  // public method
  render() {
    return (
      <button onClick={this.handleClick.bind(this)}>
        {this.text}: {this.#state.count.toString()}
      </button>
    );
  }
}

@Inve1951
Copy link
Contributor

I suggest usage of private keyword (already reserved) and stick with colons : for assignment.
As for initializers, maybe double colon prefix ::?

class Ex
  ::rand: -> Math.random()

  ::rand2: Math.random     # CS shorthand?

  private eq: -> @rand is @rand2

  logEq: -> console.log @eq()

@YamiOdymel
Copy link

Knock knock, it's already be included in Chrome 72: Public and private class fields | Web.

Private class fields

That’s where private class fields come in. The new private fields syntax is similar to public fields, except you mark the field as being private by using #. You can think of the # as being part of the field name:

class IncreasingCounter {
  #count = 0;
  get value() {
    console.log('Getting the current value!');
    return this.#count;
  }
  increment() {
    this.#count++;
  }
}

It looks like the # hash symbol will be treated as comment in CoffeeScript, so we will need another syntax for it.

Inve1951 commented on 18 Oct

I suggest usage of private keyword (already reserved) and stick with colons : for assignment.
As for initializers, maybe double colon prefix ::?

I think the double colons might be a little bit confused since it's the same syntax as static syntax (ex: Foo::bar).

@jashkenas
Copy link
Owner

Just to throw in two cents...

I think that private fields and methods are going to be another "bad part" of JavaScript, and don't really have a place in a trusted code environment like the web. If I have a handle to a JS object, I should be able to inspect and manipulate every aspect of that object, and not have some parts of it locked away.

We already have the enumerable/non-enumerable distinction, and writable/non-writable fields. I don't think private fields are necessary, or wise.

If CoffeeScript left them out, it would be for the better.

@YamiOdymel
Copy link

Well, I would say you are absolutely right.

There are bunch of the weird things in JavaScript trying to confuse the programmers. We could ignore the private fields just like how we did to const and let to keep CoffeeScript simple.

But and then we will need another document section to convenience people why CoffeeScript doesn't have private fields.

@GeoffreyBooth
Copy link
Collaborator

We’re a long way from the days of with and the other original “bad parts.” The standards groups working today seem pretty solid to me, and I think our default should be to defer to them whenever something new isn’t in obvious conflict with CoffeeScript. They go through a very rigorous process before allowing features to reach Stage 4, giving them a lot of thought from a great many stakeholders, and I think we should assume that they’re generally getting these calls right.

In particular, even if we’re pretty sure something is a bad idea, if some other part of the JavaScript ecosystem like a framework requires that feature for full interoperability, then we need to support the feature in some way. Maybe the support will be like getters and setters, via something verbose like Object.defineProperty; I’m assuming there’s an equivalent for defining private methods on a class prototype, so that very well might work today. That might be good enough, at least until private methods are a widely established feature (and a proven “good part”) that we think should be supported via some more-convenient syntax.

@laurentpayot
Copy link

I've seen several people describing CoffeeScript as a "pythonic" JavaScript. I like the pythonic approach of OOP:

Many Python users don't feel the need for private variables, though. The slogan "We're all consenting adults here" is used to describe this attitude.

@jashkenas
Copy link
Owner

if some other part of the JavaScript ecosystem like a framework requires that feature for full interoperability, then we need to support the feature in some way.

That may be true — but I thought that the point here is that private fields are private — they can't be seen, called, inspected, used, or touched by any other code. How could they be needed for interoperability?

If there's a demonstration that can be made that proves it the other way, I'll withdraw my lament.

@GeoffreyBooth
Copy link
Collaborator

How could they be needed for interoperability?

So a framework like React involves a lot of extending base classes. Those classes then get used by other parts of the React ecosystem, like if I extend React.Component and then that component gets used to render a template. In theory, some future version of React may require that users use private methods to add certain functionality to their extended classes—private in the sense that whatever other part of React that uses my new class should specifically not see my new method that I added.

So for example, say it becomes idiomatic in React to have a data method on an extended Component class, and that data method is expected to be private. Some other part of React that uses my extended Component class might throw an error when it finds a non-private data method. In other words, even though private methods by definition wouldn’t be noticed by third-party code, the lack of being hidden might cause incompatibilities with third-party libraries if those libraries are expecting me to hide certain parts of my classes.

@jashkenas
Copy link
Owner

like if I extend React.Component and then that component gets used to render a template. In theory, some future version of React may require that users use private methods to add certain functionality to their extended classes

My current understanding of the private fields proposal — (note, I can't yet test it, because it isn't yet in Chrome Canary, so this may or may not be true...) — is that "Private fields are not accessible outside of the class body".

Within the class body, you can refer to private fields on this, and also refer to private fields on other instances of the class that you're defining, but may not refer to any private field outside of the lexical body itself. That would rule out reaching into private fields in super and sub classes, as you outline...

I guess we'll find out eventually...

@jashkenas
Copy link
Owner

As an addendum, it's important to note that if subclasses were allowed to reference private fields, then they wouldn't be truly private. For any private field I wanted to get ahold of, I could simply:

class SecretStealer extends Foo {
  steal(foo) {
    return foo.#privateData;
  }
}

Which reinforces my opinion that this isn't a great part of JavaScript ... if not a bad part, then a mediocre part, at best — either this class-based syntax doesn't work at all with class inheritance, or it isn't actually private in the first place. You can't have it both ways.

@GeoffreyBooth
Copy link
Collaborator

@jashkenas To rephrase my example: If React documentation says, “define a private data method for your components,” and so then that’s what users are expected to do; then some other part of React tries to use a component I create and it sees a non-private data method and it throws an error “data should be private.” That’s what I mean. The lack of being able to define things privately might be an interoperability concern.

@xixixao
Copy link
Contributor Author

xixixao commented Dec 19, 2018

@GeoffreyBooth As Jeremy described, these are not accessible outside of the class declaration, so no API should "theoretically" require them. I think there probably will be some way of getting at these values, as should be accessible for browser tooling, but maybe those APIs will be restricted to browsers, their tools and maybe extensions (not sure, I'm not an expert).

I am not a fan of the ES proposal, simply because I have had bad experiences in the past with API authors not exposing things I needed (in languages that didn't allow reflection). Private (via mangling) fields have been used at FB for a very long time successfully, but with those I can always do instance.Class$$field in the browser console when debugging - I'm afraid this won't be as easy with the new syntax - but again, hard to know before everything is implemented.

(also, wow, totally forgot I filed this, and also, this issue was just about the compilation strategy / syntax / typing, the issue of private fields is a different beast)

@Inve1951
Copy link
Contributor

As i understand, it would transpile to a WeakMap.
I.e. (pseudo code):

class Ex
  private abc: 123
  eq: (x) -> x is @abc

being functionally equivalent to:

class Ex
  _privates = new WeakMap

  constructor: ->
    _privates.set this,
      abc: 123

  eq: (x) -> x is _privates.get(this).abc

Babel currenly offers [Class Private Instance Fields] (aren't private at all, they might have dropped a bad link) and [Static Class Fields, Private Static Methods].

Private statics are already well possible in coffeescript, the snippet above uses one.

@rdeforest
Copy link
Contributor

rdeforest commented Jul 23, 2019

Posting here for reference. 2ality has two relevant articles (so far):

Notably, these ES proposals are still at stage 3.

@GeoffreyBooth
Copy link
Collaborator

Here are the relevant proposals:

They’re all Stage 3, and most (if not all) have shipped in Node and Chrome, so they should be ready for implementation for anyone who wants to tackle them.

Obviously we need to choose a different syntax than # to denote “private.” Maybe whoever wants to work on that can start a new issue with a proposal? And a PR can follow.

@GeoffreyBooth GeoffreyBooth changed the title [Awaiting Stage 4] Class fields/class property initializers Grammar, Nodes: Class fields/class property initializers Sep 30, 2019
@edemaine
Copy link
Contributor

I went to create a new issue about this, but then found this issue exactly about instance fields. The difference is that instance fields are now in ECMAScript (both public and private, but I'm focusing on public here).

I propose modernizing CoffeeScript's output to use class fields. Consider the following input:

class Foo
  i = 1
  j: 2
  @s: 3
  sum: -> i+j+s

The current output builds a closure to simulate a private variable i accessible only within the class, a public j on the prototype, and a static member s of the class: (newlines omitted for brevity)

var Foo;
Foo = (function() {
  var i;
  class Foo {
    sum() {
      return i + j + s;
    }
  };
  i = 1;
  Foo.prototype.j = 2;
  Foo.s = 3;
  return Foo;
}).call(this);

The clearest change is that s can now be declared using static class fields syntax. This matches existing behavior for static methods (@method: -> ...).

class Foo {
  static s = 3;
}

I propose that i = 1 be translated to the new instance fields syntax (i.e. match the ECMAScript syntax exactly), and j remain as it is on the prototype:

var Foo;
Foo = (function() {
  class Foo {
    i = 1;  // note: no 'var i'
    static s = 3;
    sum() {
      return i + j + s;
    }
  };
  Foo.prototype.j = 2;
  return Foo;
}).call(this);

The difference between i = 1 and j: 2 would then be that i = 1 runs every time as part of the constructor, whereas j: 2 is assigned once at class creation time. This matters for objects:

class Foo
  fresh = {}
  stale: {}
a = new Foo
b = new Foo
console.assert a.fresh != b.fresh
console.assert a.stale == b.stale

My desire for public instance field syntax in CS output comes from supporting TypeScript (#5307). In TypeScript, you need to declare class fields via either i: number (just type declaration) or i = 0 (initializer, whose type becomes the type declaration of i) in the class body. In CoffeeScript, this would naturally be written as either i ~ number (for your favorite type declaration syntax ~) or i = 0. The proposal above reflects that.

This would be a breaking change, though. In the proposal, i would become a public member of instances of Foo, and would be accessed as @i, instead of being a variable i in the scope of the class members but nowhere else. Honestly, I didn't know that assignments within class bodies had the above behavior, but perhaps others did and exploited it. Such code would break. But is this documented behavior? The documentation says "class definitions are blocks of executable code, which make for interesting metaprogramming possibilities", which is vague, but perhaps is inconsistent with this proposal. (I'm afraid another reason I'd like to bump to CS 3.) But I'm also open to other proposals that output public class fields syntax!

@GeoffreyBooth
Copy link
Collaborator

This would be a breaking change, though.

From above:

And if or when the feature is standardized, our output could be updated to output ES2018 or whenever it lands, rather than the converted ES5 or ES2017. This is similar to the object destructuring being added in #4493.

When CoffeeScript 2 launched, there was a note (maybe just for object destructuring, or at least most prominently for object destructuring) that if/when ECMAScript caught up to us for some of the features we’re compiling down to ES5, we would update our output to match the finalized ES spec, even if it was a slight breaking change. If it’s a huge breaking change then I think we need to keep what we have, but if it’s an edge case that’s unlikely to be affecting most code then I think it’s fine to include in a semver-minor bump with clear documentation.

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

No branches or pull requests