coffeescript
coffeescript copied to clipboard
Grammar, Nodes: Class fields/class property initializers
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?
Is this the same as class properties, that are only in stage 2? See https://github.com/jashkenas/coffeescript/issues/4497#issuecomment-293724531
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!
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.
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:
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.
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.
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>
);
}
}
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()
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
).
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.
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.
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.
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.
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.
How could they be needed for interoperability?
So a framework like React involves a lot of extend
ing 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.
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...
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.
@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.
@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)
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.
Posting here for reference. 2ality has two relevant articles (so far):
Notably, these ES proposals are still at stage 3.
Here are the relevant proposals:
- Private instance methods and accessors
- Class Public Instance Fields & Private Instance Fields
- Static class fields and private static methods
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.
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!
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.