-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Revisiting getters and setters #5394
Comments
An issue is that import { get } from 'http'
get 'url', (res) -> # ... I may be misremembering, but I thought we added the error on |
I agree that Brainstorming alternative notation:
I think only the first two are not too ugly. Other ideas? |
We have the precedent that screen =
width: 1200
ratio: 16/9
@@get height: this.width / this.ratio
@@set height: (val) -> this.width = val * this.ratio Or we could use some other symbol or combination of symbols to mean “keyword.” I don’t want to use leading |
Looking ahead, Luckily, I think |
I’m not convinced that decorators will ever reach Stage 4 😉 but sure, we shouldn’t preclude a syntax that would work for them. In https://github.com/tc39/proposal-decorators#class-fields there are examples of a decorator followed by a public class field, which seems like it could be any assignment expression; like |
I was actually thinking of I'm not sure we need general syntax for "keyword" (other than back ticks?). I guess it would be useful for TypeScript's |
If |
What I had in mind was:
|
So you couldn’t have a decorator named It would be nice to avoid needing to use the rewriter. Basically we want to limit the rewriting phase as much as possible, because that file is essentially all the exceptions to our grammar rules. |
FWIW, looking at the code, this would actually be done in the lexer, in the prev[0] = 'PROPERTY_DECORATOR' if colon and prev[0] == 'DECORATOR' This is essentially working around the lookahead-1 limitation of the grammar, similar to a lot of other code in the compiler. |
Well, is In my mind “leading colon then identifier” or |
I haven't been taking a look at these various issues — and please don't take my vote as authoritative since I'm mostly just a passive observer at this point, but FWIW, my two cents are that CoffeeScript:
|
@GeoffreyBooth I generally agree, except for The @jashkenas CoffeeScript doesn't exist in a vacuum; it's part of the larger JavaScript ecosystem. TypeScript and Svelte are extremely popular, and SolidJS is growing fast. (I'm not sure the best way to measure that claim, but how about GitHub stars: 77k, 55k, and 14k respectively. For reference, CS has 16k.) Do you not want those thousands of developers to use CoffeeScript? Adding optional features to CoffeeScript that enable its use in these modern contexts seems worthwhile, like JSX which has proved immensely useful in React (180k stars) and Solid. You might also consider the chilling effect of your comments like this one; it makes me at least less inclined to help improve/modernize CoffeeScript if you don't want it to change. If you'd rather I fork into a new language, that's fine, but like @GeoffreyBooth I see a lot of value to improving CoffeeScript. |
IMO not adding basic things like getters and setters would mean that this language will never get any significant adoption again, and even adding TS support won't help. |
Sorry if I left the impression that I thought we should definitely add all those things. I was trying to sketch out what the syntaxes would be if they all were to land, just so that we don’t choose a syntax for one that might paint us into a corner if we wanted to add another. As in, it’s better to have workable syntaxes for everything we can think of that we might want to add, and choose to not add some of them, than to have suboptimal syntaxes for features we add because we didn’t consider as many features as possible in advance. Whenever we add new features there’s always the question of whether the tradeoffs are worth it. Each addition has costs:
For a feature to be worth adding, it needs benefits to outweigh these costs. In recent years just about nothing has passed this tradeoff test other than new ES features, since the learning curve there is lower (since the feature also exists in JavaScript) and it doesn’t make CoffeeScript feel meaningfully less complex since the feature is familiar from JavaScript. For an addition that’s not new ES syntax to pass this test, it generally needs to have one thing going for it: interoperability. There are plenty of languages that are beautiful and syntactically perfect and never used for production projects. CoffeeScript is not such a language. Our simplicity needs to yield to the pragmatism of needing to work with other popular JavaScript libraries and tools. People may want to use CoffeeScript because it’s clean, but they won’t use CoffeeScript if they can’t, if CoffeeScript doesn’t interoperate with some other part of their project; and users shouldn’t need to choose between CoffeeScript and other components. It’s our job to play nice, even if it means swallowing our pride and occasionally adopting things some would consider “bad parts,” such as JSX. With regard to the specific items under discussion, I feel like CoffeeScript needs a good interoperability story with SolidJS, Svelte, and TypeScript. That may involve adding a getter/setter shorthand syntax, or labels, or outputting TypeScript syntax static types; or we could find other ways to interoperate. In the TypeScript case, in particular, I would prefer we improve our comments output so that JSDoc works in all the cases where we would want it to, as this alone would solve TypeScript interoperability. And so on. Maybe the best way to use CoffeeScript with those projects is to add new syntax, but that’s not a foregone conclusion. Decorators are an unusual case. I’ve forbidden their use in projects I’ve run at work, because they’re so unstable and on an especially uncertain trajectory; there’s no way that CoffeeScript will support them anytime soon, if we ever do. (And if JavaScript itself ever does, for that matter.) However, two popular web components libraries, Lit and Stencil, both use decorators extensively. Presumably they will continue to do so once decorators eventually stabilize, and we’ll be forced to support the syntax somehow in order for people to use CoffeeScript with those libraries, unless there’s some easy workaround (such as an intuitive way to convert a decorator into a higher-order function). It’s not enough that the user could look at the JavaScript transpiled out of those libraries’ decorators and write CoffeeScript against that; the same was always true with React and JSX, but that’s too terrible of a user experience to consider acceptable, especially when we want users to see examples in JavaScript for these libraries and be able to easily write the equivalent CoffeeScript. So anyway I think the bottom line is that I understand the desire to keep CoffeeScript pure and clean, and we should try to do so as much as possible; but CoffeeScript isn’t on a pedestal, and it needs to be used to survive, and that means we need to follow the trends somewhat and interoperate with the popular libraries. It’s unfortunate that getters and setters are having a renaissance (another web components library, Polymer, uses getters and setters extensively) and I never would have guessed that labels would be rescued out of Douglas Crockford’s recycle bin, but here we are. If there’s a way to support CoffeeScript users of those libraries without adding more clutter to CoffeeScript, that would be ideal, but ultimately I think interoperability is a higher priority than purity. |
Like I said — please don't take my vote as authoritative. Y'all are running the show here, and @GeoffreyBooth has the commit bit. I'd like for you to continue to develop things in the way you think is best. |
I agree with almost everything @GeoffreyBooth says. It is so refreshing to see a popular project being maintained by such a rational, receptive and reflective person! Do we even want to encourage programmers to build new projects with CoffeeScript? If the answer to this question is yes, then imo there is no way getters/setters can stay unsupported. Can we lead the same discussion for every new addition to the language? Keeping to the current strategy of supporting every reasonable feature of JS as soon as it hits stage 4 (consequently possibly including decorators some day) is the way to go, I'd say. If not, CoffeeScript's only future prospect is maintenance of legacy codebases, and it should have never done v2. Legacy codebases can still happily continue to use traditional CoffeeScript, regardless of whether we add getters or TypeScript support. Regarding the latter: While Svelte or React will die at some point, TS arguably never will, not until JS itself is dead, so there's no way around it. But I'm also unsure if it should be core language or a well highlighted fork or a new compile flag etc. |
This is getting a little off topic for this thread, but given the mention of a compiler flag for TypeScript: this might be worth reconsidering. I've been noticing at the least that the output file really only makes sense with a |
I think this is definitely on the table; this was on my mind on the TypeScript thread when I said I wanted to avoid breaking changes at all costs. There’s a lot of other messiness and caveats with adding a flag that I don’t want to get into here, but I think first we should get as far as we can with improving output that doesn’t involve new syntax (so the Getting back to getters and setters, before I initially commented on this thread I reread the old thread from the CoffeeScript 2 discussion, and basically my takeaway was that at the time we thought that the feature was one of JavaScript’s “bad parts” but only mildly so, and since there didn’t seem to have a strong need for the shorthand syntax that we could make do with the verbose syntax and stop there. (It’s worth remembering that CoffeeScript has always supported getters and setters, just via the Here’s an example of getters in the Solid docs: const mapped = indexArray(source, (model) => {
return {
get id() {
return model().id
}
get firstInitial() {
return model().firstName[0];
},
get fullName() {
return `${model().firstName} ${model().lastName}`;
},
}
}); How would this be achieved in CoffeeScript today? Probably something like this? mapped = indexArray source, (model) =>
Object.defineProperties {},
id:
get: -> model().id
firstInitial:
get: -> model().firstName[0]
fullName:
get: -> "#{model().firstName} #{model().lastName}" You could even get fancy and inline the value of each property: mapped = indexArray source, (model) =>
Object.defineProperties {},
id: get: -> model().id
firstInitial: get: -> model().firstName[0]
fullName: get: -> "#{model().firstName} #{model().lastName}" Now this looks awfully similar to some of the proposed new syntaxes. So then the question becomes, is The example in the docs only shows |
Cool, I didn't know about Other than working with TypeScript today, I think where a get/set notation would really benefit is when you have a mix of regular properties and get/set properties, as in the example just above the one you mentioned in the Solid docs: const mapped = mapArray(source, (model) => {
const [name, setName] = createSignal(model.name);
const [description, setDescription] = createSignal(model.description);
return {
id: model.id,
get name() {
return name();
},
get description() {
return description();
}
setName,
setDescription
}
}); In CoffeeScript today, that would look something like this: mapped = mapArray source, (model) ->
[name, setName] = createSignal model.name
[description, setDescription] = createSignal model.description
Object.defineProperties {id: model.id, setName, setDescription),
name: get: -> name()
description: get: -> description() It's not terrible thanks to mapped = mapArray source, (model) ->
[name, setName] = createSignal model.name
[description, setDescription] = createSignal model.description
id: model.id
@@get name: -> name()
@@get description: -> description()
setName: setName
setDescription: setDescription It'd also be a lot easier to switch between regular properties and get/set properties when the notation is like this. There were a bunch of ideas in the old thread about property notation that specifies a property with a nested object with id: model.id
@@name: get: -> name()
@@description: get: -> description()
setName: setName
setDescription: setDescription
@@automagic:
get: -> ...
set: -> ... This is farther from ECMAScript syntax, though, and a bit harder to parse. |
That’s the TypeScript playground, so it’s expecting TypeScript input. If you paste it into VS Code as JavaScript and hover over If property order is a concern, you could declare all the properties in the initial object declaration, with the ones that will become getters set to |
You can actually switch the TypeScript playground to JavaScript input mode under the "TS Config" menu. If you also turn on So this isn't really a great solution. TypeScript seems fine with omitting initializers ( Regarding order: I didn't mean the actual order in the object (though I guess that could be a concern). I meant the order in which you want to write your code. It's nice for the programmer to have design agency here, for example, if they wanted to put the definitions of |
You could define the entire type explicitly: /**
* @typedef Rectangle
* @property {number} width
* @property {number} ratio
* @property {number} [height]
*/
/** @type {Rectangle} */
var rectangle = Object.defineProperties({
width: 1200,
ratio: 16 / 9
}, {
height: {
get() {
return this.width / this.ratio;
},
set(val) {
return this.width = val * this.ratio;
}
}
});
console.log(rectangle.height); And this has no errors; the explicit JSDoc overrides the implicit typing. As for Anyway, yes, the core argument for the shorthand syntax has always been that it looks nicer or is easier to read or allows prettier code, or however you’d like to frame the desire for a clearer way to express the definition of getters and setters. And forcing users to use ugly syntax instead is not so subtly implying that getters/setters are frowned upon and shouldn’t be used. So we’re back to the tradeoffs: is adding a syntax for the shorthand syntax worth the costs. I don’t know. |
If you're going down the class Foo
@@random: -> Math.random()
@@name: -> Db.query 'name'
@@name: (newName) -> Db.update 'name', newName I don't particularly like how close it is to What about a named attribute like class Foo
property random: -> Math.random()
property name: -> Db.query 'name'
property name: (newName) -> Db.update 'name', newName This might be easier to implement since it's basically a function with an object argument. You could also go down a similar path to some other languages and add a symbol after the slot name: class Foo
random^: -> Math.random()
name^: -> Db.query 'name'
name=: (newName) -> Db.update 'name', newName
I also think hacking the computed property syntax looks ok but would still lead to conflicts albeit inside of computed property expressions only. class Foo
[get random]: -> Math.random()
[get name]: -> Db.query 'name'
[set name]: (newName) -> Db.update 'name', newName For that matter, I don't mind the |
CoffeeScript really needs to implement getters and setters. And, it should work just like the JavaScript version - no hacks, please. For some reason, the assumption is made above that a word like 'get' or 'set' must be either a keyword or an identifier (i.e. variable name, function name, etc.). There are plenty of languages (PL/1, I seem to recall) that allow any keyword to also be used as the name of a function, variable, etc. It's all down to how the parser is implemented, and admittedly, I don't know the internals of CoffeeScript. The following BNF should work:
i.e. when the parser is looking for the name of a property, it gets the first token. Then it gets another token. If the 2nd token is ':', the first token is the name of the property and it's not a getter or setter. If the 2nd token is an identifier, then it's the name of the property and 1) the first token must have been 'get' or 'set', otherwise it's an error, and the next token must be a ':' or it's an error. |
I'm starting to work with parser generators again, and I've come to a realization regarding my last comment. The examples I've seen make, what I think is a mistake in their parser definitions. That is, they ask the lexer to make a distinction which should really be done by the parser - whether a word it sees is a keyword or an identifier. If the lexer makes that distinction, then it's not possible (or at least very difficult) for the parser to allow a keyword to be used as the name of a variable or function. I think that the lexer should only have to say "I found a word - and here is the string version of it". The parser should handle treating it as a keyword or identifier, depending on the context in which it finds that token. Now, I admit that it's generally a bad practice to use a keyword as an identifier, e.g. creating a variable named "if". But here is a clear case where, because the words 'get' and 'set' have been used as names of functions/methods in the past, we want to allow that and also allow 'get' and 'set' as keywords. That should be possible. I just did some googling and though I can't really vouch for the source, it appears that although JavaScript won't let you name a function 'if' or 'while', it will allow you to name a function 'get' or 'set'. So, clearly, the JavaScript parser doesn't force a word to be either a keyword or an identifier. |
This is a feature request. I think it's time to revisit getters and setters, originally discussed in #4915, and explicitly unsupported on https://coffeescript.org/#unsupported-get-set
Input Code
Expected Behavior
Current Behavior
error: 'get' cannot be used as a keyword, or as a function call without parentheses
Motivation
I have two arguments for why getters and setters should be added to CoffeeScript.
One argument is increased recommended usage. Previously @GeoffreyBooth argued that getters/setters seemed likely useful in reactive JavaScript libraries, but "my worry is hypothetical, so maybe it’s not worth worrying about until there’s a concrete example". Now a concrete example is SolidJS, an increasingly popular reactive library for building user interfaces, which uses getters/setters extensively as a way to detect reactivity. For example, components get a
props
argument, and use ofprops.name
in e.g.<h1>Hello {props.name}</h1>
automatically reacts to changes to thename
property. In the case ofprops
, that's all done by SolidJS automatically, but the SolidJS documentation encourages using getters extensively (try searching the page forget
including the space). Specifically, in order to build your own reactive objects that work just like the built-in ones, you build an object with getters and/or setters. In SolidJS, use of reactive data needs to be wrapped in a closure for automatic reactivity to work. For single states, this is done via a "getter" function (a function that takes no arguments), which is fine in CS; but for complex state with multiple attributes, it's done by building an object with multiple getters.(Incidentally, other than this issue, writing SolidJS apps in CoffeeScript works well. In fact, early versions of SolidJS were written in CoffeeScript! But before the move to getters/setters.)
The second argument is better TypeScript support (for #5307). The recommended workaround for getters and setters in CoffeeScript is to use
Object.defineProperty
. But TypeScript doesn't understand the effect ofObject.defineProperty
. It's not even possible to provide an explicit type because a single assignment to the variable must provide all the properties at once, andObject.defineProperty
can only do one at a time and only after the object has been initialized.Context
Conveniently, CS2 already made
get foo: ->
syntax invalid, and similarly forset
. (Actually,get foo: ->
alone is valid, but it becomes invalid if you add a blank line before it. This is presumably a bug.) It was reserved for this very purpose:get
/set
became useful enough to add (and to prevent confusion if someone guessed at the correct notation). So, we can use the obvious syntax and all CS2 code remains compatible.A
get
/set
prefix would also go well withprivate
,protected
, andpublic
annotation prefixes, which are TypeScript features. (Note thatprivate
,protected
, andpublic
are reserved words in CoffeeScript, so they could be added while being backward incompatible. It's a separate discussion whether they are worth adding.)Alternatives
If the ambiguity with calling functions
get
orset
is considered too confusing, we could consider a new syntax that is not ambiguous. For example, inspired by ideas in #5367, we could add a:
prefix:I personally find this uglier, so would prefer straight
get prop:
/set prop:
syntax.The text was updated successfully, but these errors were encountered: