jashkenas / coffeescript

Unfancy JavaScript
https://coffeescript.org/
MIT License
16.49k stars 1.98k forks source link

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

Open xixixao opened 7 years ago

xixixao commented 7 years ago

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 commented 7 years ago

Is this the same as class properties, that are only in stage 2? See https://github.com/jashkenas/coffeescript/issues/4497#issuecomment-293724531

connec commented 7 years ago

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:

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

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 commented 7 years ago

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 commented 7 years ago

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 :disappointed:

GeoffreyBooth commented 7 years ago

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 https://github.com/jashkenas/coffeescript/pull/4493.

GeoffreyBooth commented 6 years ago

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 commented 5 years ago

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. https://github.com/tc39/proposal-private-methods/commit/7aa58d7af3441d4558bd73bbad315c4ab21899ae

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

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

Notably, these ES proposals are still at stage 3.

GeoffreyBooth commented 5 years ago

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.

edemaine commented 2 years ago

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 commented 2 years ago

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.