-
Notifications
You must be signed in to change notification settings - Fork 4
CS2 Discussion: Output: Classes #22
Comments
This is discussed in jashkenas/coffeescript#4233 (and probably many others I'm not aware of). Also, I just discovered this Redux PR thanks to @GeoffreyBooth which looks REALLY promising. |
So it appears that someone has actually already implemented CoffeeScript |
I think there's probably a larger conversation to be had about decaffeinate specifically, how and where it intersects with this project, what can be borrowed, what can be adapted, etc. |
Hi, I'm a decaffeinate contributor. While decaffeinate does have basic support for classes, full support is one of the main missing features right now. I'm hoping to improve it over the next few weeks, but it's definitely a challenge, since my understanding is CoffeeScript allows arbitrary code in class bodies, while ES2015 classes only allow methods. Decaffeinate also compiles CoffeeScript class fields to the public class fields proposed feature (currently stage 2), but even that has pretty significant semantic differences that have caused bugs for me (CS class fields are on the prototype, JS class fields are assigned to the instance once the constructor finishes). Anything other than methods or fields causes decaffeinate to crash. |
Thanks @alangpierce. Yeah, after looking at it more I think the better solution is to redefine the While the immediate focus is on adding support to CoffeeScript for missing vital ES2015 features (modules, classes), the longer-term goal especially of coffeescript6 is to update CoffeeScript’s output to use more of the latest standards. This essentially means we would be doing the work that decaffeinate is doing, but within the main CoffeeScript compiler. I skimmed through the decaffeinate repo, hoping to find some commits that highlight what differences it had from the main CoffeeScript compiler, but I couldn’t make sense of it. Do you mind giving whatever overview you can of how decaffeinate works, and where I should look for code that might be useful to repurpose in the main CoffeeScript compiler? For example, where is the code that generates an ES2015 class? |
I would love to see some way of extending the current class functionality to generate the ES2015 code, and then decorate with everything else. I guess I don't really understand what actually breaks and where yet though. I am assuming that ES classes have extra restrictions, ie that they aren't just objects with prototypes set in a specific pattern? From the Babel overview:
So I would think we could construct functions only, then decorate with everything else? |
@GeoffreyBooth Yep, dropping support for code in class bodies makes a lot of sense for your case. Sure, I can give a quick overview of how decaffeinate works: Unfortunately, I think the short answer is that decaffeinate's approach is pretty different from the CoffeeScript compiler, so you may not be able to use much code/logic. From my reading of the CoffeeScript compiler, it parses the input to an AST, then calls the recursive A little more information on how decaffeinate is structured: it consists of five "stages" (one CoffeeScript -> CoffeeScript, one CoffeeScript -> JavaScript, and three JavaScript -> JavaScript), the most important of which is the CoffeeScript -> JavaScript stage, which is called MainStage. For that stage, every type of AST node corresponds to a Patcher class. Each Patcher is a class that knows what kind of modifications to make to transform that type of CoffeeScript node to JavaScript. Here's a repl link that has some examples of what happens in different stages. You can change the "run to stage" selection to see the intermediate result after each stage (or intermediate lexer/parser results). I actually haven't worked with the class generation code, but was able to mostly figure out how things work. If you look at the decaffeinate-parser output for your example, you can see that the main AST hierarchy is Also, here's a writeup with some more details: Hope that helps! |
I don’t think we can simply redefine the How about this approach: we create a new CoffeeScript keyword, With Introducing a new keyword that wasn’t previously reserved is a breaking change, so we would need to introduce a flag. I think since
We could even add support for making Conversely, it might be a good idea to introduce What do you all think? @lydell, @rattrayalex, @JimPanic, others . . . |
I like the idea of Apart from that, I think the only solution to introduce this to users is a) with conversion tools and b) through a process of deprecation warnings, deprecation and removal over several releases. This is something we have to plan, no matter what keywords we use in the end. The only thing that could be dangerous is to keep the |
Here's my vote: Add Release it by bumping the middle version number, keeping CoffeeScript's current versioning scheme of bumping the middle number when there are new features an slightly-but-not-very breaking changes. (As @GeoffreyBooth it is very unlikely that this will cause any trouble for anyone.) No CoffeeScript 2.0.0: Remove old |
I guess we probably don’t need a flag if we’re simply adding My god, did we just come up with a way to implement classes without a flag? 😛 |
I think we did, yes. 🤔 I'm also very much in favour of implementing semver. |
I like the idea of a separate At present Example:
|
@greghuc I don’t really see the step backwards? # ClassExample.coffee
CONSTANT_EXAMPLE = 'I am a constant example'
privateStaticFunction = -> console.log 'I am a private static function'
export class ClassExample
constructor: -> console.log 'I am a constructor'
instanceFunction: ->
console.log '[start] I am an instance function'
console.log CONSTANT_EXAMPLE
privateStaticFunction()
@constructor.staticFunction()
console.log '[end] I am an instance function'
@staticFunction: ->
console.log '[start] I am a static function'
console.log CONSTANT_EXAMPLE
privateStaticFunction()
console.log '[end] I am a static function'
# whatever.coffee
import {ClassExample} from 'ClassExample.coffee'
ClassExample.staticFunction()
(new ClassExample).instanceFunction() |
@lydell I'm still getting up to speed with ES6 features, so please take my comments with a pinch of salt.. Your example code isn't that bad. It is essentially forcing a coffeescript user to have a closure wrapping both the class declaration and the private 'constant' and static function, to ensure private means private. But from my quick scan of ES6 modules, I suppose there's an implicit closure when importing a module from a file. I could live with this, although I don't think your example code is as clean or explanatory as the current coffeescript code. In the current code, the class acts as an encapsulating namespace for all private and public functions. For the |
How about, instead of |
@DomVinyard In my opinion that would be very confusing. I also think that that will break more people's code – even CoffeeScript's source code has |
@lydell I concede that point and do accept it would break more projects than |
@DomVinyard I have to agree with @lydell on this one. I’m not thrilled by the name So I think the next step is to define what the syntax of
Here’s an extensive example. It seems clear from the examples that we will need to implement I think like with modules, our goal should be to simply create a way to output ES classes with all their features as they are, rather than trying to improve upon them the way that the original Is anyone else interested in helping research/define this, and/or code it? |
Exciting stuff. Perhaps once I am curious though – what advantages do CS classes have over ES classes? Why would a developer prefer CS to ES output? (Honestly don't know). |
@rattrayalex ES201x classes cannot have executable code in their bodies. You cannot declare properties at runtime. You can do that with CS classes but I personally think it's bad practice to do so as it's not very explicit and mixes different styles of defining "types". |
Ok let me rephrase that a bit: of course you can declare properties at runtime, but not in the class declaration body like in CS. :) |
Re-reading through this 'classes' thread, I think it's worth noting that are at least three distinctive positions to take on Coffeescript and ES6 classes. Two positions are at odds with one another. The positions:
My personal position is a combination of 1 and 3: change the Coffeescript compiler to take advantage of ES6 class functionality, and fix any technical incompatibilities. But don't change (or remove!) existing CS class semantics. I see position 2 as a risky one (Coffeescript classes are semantically wrong, now that ES6 classes have come along). This path leads to the ES6 tail wagging the Coffeescript dog. From now on, Coffeescript is no longer an opinionated "little language that compiles into JavaScript". Instead, we'll be continually playing catchup, discussing the best way to implement the newest EcmaScript feature in Coffeescript syntax. In essence, it will be Ecmascript committees driving the core direction of Coffeescript, not the Coffeescript committers themselves. Already, because ES6 classes don't currently support data properties, we might have to remove Coffeescript class data properties. What happens when Ecmascript adds them back in? |
@greghuc The biggest problems, in my opinion, are that you currently cannot extend an ES2015+ class (jashkenas/coffeescript#4233) and that CoffeeScript’s |
Also, to put a slight dampener on 'ES6 classes are the new awesome', here's a link to a curated list of resources on why ES6 (aka ES2015) classes are NOT awesome. I'm not aiming to start an argument over the pros and cons of ES6 classes. However, it's worth keeping in mind that ES6 classes are a new and unproven technology, and it's unclear how things will play out with them. This is independent to how well ES6 classes are supported by browsers and nodejs. This raises the possibility of Coffeescript nextgen locking itself to a feature that doesn't stand the test of time. It doesn't preclude Coffeescript being able to interoperate with ES6 classes. |
@greghuc I think your preferred approach (1 and 3) is what the What your “data properties” example illustrates, however, is that we should think long and hard before we add any sugar over I don’t think it’s so bad to follow ECMA. They’re moving way faster than we ever could. It’s not a bad thing to keep up with the JavaScript world. |
I’ve been thinking about how Decaf manages to parse most CoffeeScript classes into ES2015 Out of curiosity I ran Decaf on It’s another question whether these ES2015 Assuming we wouldn’t use Decaf but would reimplement similar logic ourselves, is this an approach we want to take? |
@mrmowgli regarding Firstly, the way Context.name = -> super # doesn't compile
Context.prototype.name = -> super # super reference: Context.__super__.name
class Context then @name = -> super # super reference: Context.__super__.constructor.name
class Context then name: -> super # super reference: Context.__super__.name
# constructor is not special
Context.prototype.constructor = -> super # super reference: Context.__super__.constructor
class Context then constructor: -> super # super reference: Context.__super__.constructor In jashkenas/coffeescript#4354 (the active ES class PR) the only change for class Context then constructor: -> super # super reference: super Ordinarily, the "super reference" is compiled with a
That is an interesting case indeed. The way I chose to handle it in my PR is to add an implicit class A
class B extends A then constructor: ->
# ...
# class B extends superClass {
# constructor() {
# super(...arguments);
# }
# }
# ...
class A
class B extends A then constructor: -> super()
# ...
# class B extends superClass {
# constructor() {
# super();
# }
# }
# ... The reason I chose to do this is because it's the most reasonable way I could think of to deal with class B extends A
constructor: (@name) ->
greet: ->
console.log "Hi #{@name}"
class C extends A
constructor: (name) ->
super()
@name = name
greet: =>
console.log "Hi #{@name}" Without an implicit class B extends A {
constructor (name) {
this.name = name
// super() - this is already too late
}
}
class C extends B {
constructor (name) {
this.greet = bind(this.greet, this)
super() // - this is also too late
}
} Given these cases, there are basically 3 options:
I went with an extension of 3., and decided to add an implicit |
Sorry for the wall of text! Figured I'd try and explain the current status as fully as I could. Having written that all out, I'm starting to feel like option 2. should be explored a little more. I guess the idea behind that would be to generate code that made it seem like // class A
// class B extends A
// constructor: (@foo) ->
// console.log super(), @foo, @bar
// bar: =>
class A {}
class B extends A {
constructor (foo) {
var ref;
console.log((ref = super(), this.foo = foo, this.bar = this.bar.bind(this), ref), this.foo, this.bar)
}
bar () {}
} This would behave reasonably well I think, as attempting to access |
Ok, my bad: Super CAN be called in other methods, and var A = class A {
constructor (token) {
this.token = token;
console.log('Token in base was: ', token);
}
objectLocal(origin) {
console.log('The '+ origin + ' called in Parent method.');
}
}
var B = class B extends A {
constructor (token){
super(token);
this.token = token;
console.log('Secondly, token `'+ this.token + '` was called in child.');
}
objectLocal() {
console.log('Overriding objectLocal now with "super"');
console.log('Current token: ' + this.token)
super.objectLocal(this.token)
}
}
let n = new B('coffee-script');
n.objectLocal();
/* Output:
Token in base was: coffee-script
Secondly, token `coffee-script` was called in child.
Overriding objectLocal now with "super"
Current token: coffee-script
The coffee-script called in Parent method.
*/ |
@connec @mrmowgli, fantastic heroic work on jashkenas/coffeescript#4354. Now that that’s merged in, what work related to classes remains to be done? |
I realize we didn't create browser specific tests, however I believe we are expecting Babel to cover the bases there. Other than that it seems like it's mostly edge cases surrounding super(), and those need actual usage to shake out a bit. I'm of the opinion we should go ahead with the release, the tests are comprehensive and everything works far better than I originally expected. @connec did an amazing job! Realistically I believe we should start trying to cover lower priority items like get/set, species, and new modifiers like |
The only thing we probably want to finish off for Unless someone else picks it up I will work on this once I make some time. |
@mrmowgli the tests really only cover Node, aside from the browser-specific ones. We treat Node as more or less our reference JavaScript runtime to test against, for JavaScript features that should behave identically in Node and browser-based runtimes. @connec what do you mean by |
CS source class Base
method: ->
console.log 'Base#method'
class Child extends Base
method: ->
super
console.log 'Child#method' Current CS2 output // Generated by CoffeeScript 2.0.0-alpha
var Base, Child;
Base = class Base {
method() {
return console.log('Base#method');
}
};
Child = (function(superClass) {
class Child extends superClass {
method() {
Child.__super__.method.call(this, ...arguments);
return console.log('Child#method');
}
};
Child.__super__ = superClass.prototype;
return Child;
})(Base); Desired CS2 output // Generated by CoffeeScript 2.0.0-alpha
var Base, Child;
Base = class Base {
method() {
return console.log('Base#method');
}
};
Child = class Child extends Base {
method() {
super.method(...arguments);
return console.log('Child#method');
}
}; I could have time any day now 😄 Watch this space at the weekend... |
There will be some 'tough choices' to make for this, I expect. I've not explored it much yet but if we wanted to clean up properly we would effectively lose our current support for Base = ->
Base::method = -> console.log 'Base#method'
Child = ->
Child extends Base
Child::method = ->
super
console.log 'Base#method' Some alternatives:
I'll flesh all these out a bit once I started looking at it. |
We already removed some tests that assumed a class was really a function (i.e. the pre-ES2015 When you get a chance, you should start documenting the class-related breaking changes in the wiki. You’re far more familiar with what’s changed than I am. |
ES6 Edit: Regardless — moving to ES6 classes for CS2 means that you take the lumps that come with. We should definitely not still have the original system still lying around in the codebase for the CS2 release. |
Nope @jashkenas, it's a 'syntactic form' that is only valid within a class initializer. |
I think it's interesting that we could opt to compile |
Threw together jashkenas/coffeescript#4424 with the bare bones. |
It's just madness. It straightjackets class construction unnecessarily, and makes ugly lots of interesting patterns for mixing methods into classes, or composing traits. One step forward, three steps back.
That seems like a pretty nice CoffeeScript feature that would improve upon ES6 classes. |
Closed via jashkenas/coffeescript#4424. For further changes or improvements upon CS2 classes, please open new issues. |
Migrated to jashkenas/coffeescript#4922 |
This is a core feature that we should rally around submitting to jashhkenas/coffeescript in the near term.
Does anyone know of current leading efforts to accomplish this in the community? Perhaps we can lend a hand there.
The text was updated successfully, but these errors were encountered: