Closed jashkenas closed 12 years ago
Besides a few typos (log: (err)
-> log = (err)
, this.prototype.[name]
-> @prototype[name]
), I think this is a great path to continue pursuing.
That said, I have my concerns.
@
and class/static methods with @@
, as they are referenced in ruby.initialize
method? There is also an attempt to remedy this in my old proposal.In any case, even if the community does come to an agreement on the semantics and syntax of this new feature, I want to stress early on that we should really give this one a good amount of time in hardening before merging it onto master. We've seen how big changes like this getting pushed through too fast can harm the community and I don't want it to happen again, no matter how much I am in favor of this change.
1) The syntax does allow for local (private) methods.
class Person
privateFunc = ->
publicFunc: ->
Compiles to:
var Person;
Person = function() {
var privateFunc;
function Person() {}
privateFunc = function() {};
Person.prototype.publicFunc = function() {};
return Person;
}();
2) I'd love to hear what your problems are with the output JS -- it's more or less the same as the current output for classes.
3) Yes, you can define the constructor function.
class Animal
(name) ->
@name = "Animal " + name
Compiles into:
var Animal;
Animal = function() {
function Animal(name) {
this.name = "Animal " + name;
}
return Animal;
}();
:
operator? Is it inconsistent then to use @static = ->
rather than @static: ->
? Also, this strays from how all other variables / functions are defined elsewhere. My proposal (I hate to keep promoting my own work like this, I sound like some of my old professors) did not have that problem.ClassName.prototype
. But doing it that way may not be best. Either way, we should not worry about it until after we agree on syntax / semantics (which I now see are more fully defined already in the current proposal than I thought).A note I would like to bring to this thread from one of StanAngeloff's posts in the last thread is that @
symbolizes an instance everywhere else. That may be a small point of confusion for newcomers / unfamiliars. A counterexample to this, though, is ruby's usage of self
inside instance methods (referring to the instance) and inside its executable class bodies (referring to the class).
I can't wrap my brain around the syntax, it doesn't make any sense. Why would an implicit object in a block be turned into a prototype function?
class Klass
if env is 'development'
randomKey: -> 'I am not an object, I am a class member.'
I like the idea, but the execution is poor. For one, accessing this
is inconsistent:
class Klass extends Base
@methodOnThis()
anotherMethodOnThis: ->
When you are using the @
you are referencing a method on this
, but to define new methods, you simply plug the name in an object bag.
Static and instance properties are also inconsistent:
class Klass extends Base
@methodOnParentFromInstance()
@staticPropertyOnKlassNotThis = 'static'
memberPropertyOnThis: -> 'instance'
Here @
represents two things, either a method invocation on the instance or a static property on the class. The assignment syntax also differs on static vs. instance members.
Having the constructor hanging around as an anonymous function at the top level implies it can't be part of a conditional statement, such as if env is 'development'
? I'd much rather see the old constructor
back and yes, we have means to override the constructor already (a named function is generated).
EDIT: mid-air collision with michaelficarra, same points there.
I'm glad you decided to give executable class bodies another go. I'm with Michael, though - this needs to be thought through carefully before merging with master.
So what you're doing here is taking Stan's proposal of manipulating the class definition context and creating a syntactical metaphor around that, but going one step further and having the context be the static class instead of the class prototype. I assume your goal was at least partly in reducing the amount of operators required, eg. @@ for self (static) vs. @ for this (prototype).
foo: bar
as an alias for @::foo = bar
. Might not be bad, because you're bringing the prototypal aspect of class creation a bit closer to the user. You need to note, however, that this is only dressing up the operator problem a bit differently. I applaud your efforts in minimizing the amount of keywords and syntax to be learned, but having @ mean two different things in close proximity is dangerous unless you come up with a good enough metaphor for describing the feature. Are @ and : while using @ for two things, as opposed to @@ and @, really such a good solution?constructor
?attr
to all classes. Might metaclasses be possibly applicable in the context of Coffeescript, or can we learn something from their application in other languages? On a conceptual level, I think they'd be about declaratively affecting the scope of class body execution and therefore the available static methods.Mixins, as their implementation currently stands, do not provide an adequate solution for integrating metapgrogramming capabilities, because their contents are only applied after class declaration. For this feature to be complete and stand on its own, I think something additional is required.
I don't like the syntax either. I don't like the idea of dropping constructor
- it looks inconsistent from the definitions of every other method on the class.
Why are executable bodies even necessary? You can accomplish all of the examples here with the current class syntax.
Why write:
class Logger
if env is 'development'
log: (err) -> console.log err
else if env is 'production'
log: (err) -> console.error err
when you can just write:
class Logger
log: (err) ->
if env is 'development'
console.log err
else if env is 'production'
console.error err
it is simpler, and makes more sense.
The metaprogramming bit is nice, but isn't something that can't be accomplished using ordinary CoffeeScript. Just do the same thing in the constructor
function and you are fine.
class Model
attr: (name) ->
this[name] = (val) ->
if val?
@["_" + name] = val
else
@["_" + name]
class Robot extends Model
constructor: ->
@attr 'power'
@attr 'speed'
#...
Maybe I am missing something here, but it doesn't seem to me to be that much simpler, or better in any way to what we already have.
$ bin/coffee -bsp
class A
privDict = if inv then (v): k else (k): v
var A;
A = function() {
var privDict;
function A() {}
privDict = inv ? A.prototype.v = k : A.prototype.k = v;
return A;
}();
Intended?
I adopted satyr's naked constructors because they get rid of yet another special keyword, while providing a syntax closer to what the constructor function really is -- i.e. the class object. For example:
# constructor function:
Person = (name) ->
@name = name
# class form:
class Person
(name) ->
@name = name
... but if the consensus is that it's better to keep the constructor
label, that would certainly be an easy change to make.
Another change we could make is the switch of static member assignment from =
to :
. If we made it work with both, the syntax for these executable classes would be identical to what we currently have on master and 0.9.4.
Finally -- satyr's point about literal objects is well taken, we need a rule for distinguishing the uses. On his original branch, only top-level object-literals were treated as prototype properties, but that doesn't make executable classes very useful. Perhaps we can say that an object literal to the right-hand-side of an assignment is just an object, and is a prototype property otherwise...
Wow! I hadn't been blown away with a new proposal since that "implicit object properties" one.
Conditionally define methods
Great! Especially useful when you don't want to define some functions in specific contexts -- like when using the same module in both client and server.
\ Naked constructors -- no constructor keyword needed **
class Animal
(name) ->
@name = "Mr. " + name
I like it. Why make things more verbose than they need to be?! Simple, visually clear. What CoffeeScript aspires to be.
Metaprogramming
class Model
@attr = (name) ->
Very useful example. This is perfect for defining models.
My Conclusion
I'm very excited by this proposal! The metaprogramming feature especially solves some big problems I've been having when defining data models.
There are some things to iron out, I know, but I love what just became possible!
The biggest benefit here in my mind is the static inheritance. The executable body and metaprogramming possibilities are great, but static inheritance is a very basic feature that I've been waiting for.
Great news.
Instead of eliding the constructor:
label altogether, why not change it to new:
? Short, logical, mirrors use, and already a keyword.
That's an interesting proposition, sethaurus. Possibly confusing, because new
would not actually handle instantiation but merely initialization - new Foo
would not mean Foo.new()
. Refer to the way new
works in Ruby.
But in JS and CS— and unlike ruby — instantiation and initialization are already a single, indivisible step. The syntax for constructing an instance, new Thing()
, is doumented. Labelling the initializer with constructor:
doesn't imply that you create a new instance by calling Thing.constructor()
, so nor should new:
imply Thing.new()
. Since we need a pseudo-method to be invoked by the new
operator, calling it new:
reduces the cognitive overhead, at least in my mind.
Alright ... pushed some tweaks to executable
. The naked functions are gone, and constructors are now defined with constructor:
as before. The only syntax difference from master now for simple classes is @static = val
...
I've also made it so that although defining prototypal properties is the default, you can also force the use of private object literals, simply by using explicit braces. Hopefully things will compile more or less as expected:
class A
privateUrls =
home: '/home.html'
go: ->
load privateUrls.home
Into:
var A;
A = function() {
var privateUrls;
function A() {}
privateUrls = {
home: '/home.html'
};
A.prototype.go = function() {
return load(privateUrls.home);
};
return A;
}();
Altogether, the proposed changes look great to me. Improvements all around.
I'm chiming in a bit late, but I really liked the naked constructors. The approach reduces verbosity, frees up a keyword, and makes constructors look (aptly) more distinct from other functions.
Perhaps most importantly of all, it reduces cognitive effort—every time I type constructor
, I think to myself, "Do I have that right?" It would be more sensible to either adopt the C/Java convention of reusing the class name, or perhaps the Python __init__
, rather than using a long word that appears to define an ordinary function.
The constructor
keyword approach has always felt awkward to me, and I think satyr's solution is quite elegant.
What TrevorBurnham said. :)
Ok -- after messing around with a bunch of different test cases, I'll put the naked constructors back in. If you want to discuss the alternatives, this afternoon is a good time to speak up in #coffeescript
...
Since overall reception to this branch has been overwhelmingly favorable, and I think it's worth nailing down, I've merged executable back into master
, and we can continue to refine it there. Leaving this ticket open until we have the final form of this syntax nailed down.
I, like StanAngeloff, am still not happy with the current syntax of just putting object literals in the class definition to define methods. Why can't we just use equality and the @
, which is used everywhere else to mean instance method?
I've also made it so that although defining prototypal properties is the default, you can also force the use of private object literals, simply by using explicit braces.
I'm ESPECIALLY unhappy that there's a semantic difference between implicit and explicit objects. That one, I cannot stand for.
And although everyone, myself included, does feel very positive about this new feature, I really don't think it should be merged to master
just yet. It sets a precedent for merging in huge changes like this less than 48 hours after they were proposed. What's so bad about keeping it on a separate branch?
Finally, I liked that we were using the constructor
name to define constructors. Naked constructors are okay, but I usually like being a little more explicit, especially if we would be using the @
syntax for instance methods. I think an even better name for constructor
would be new
, that way we can define a ClassName.new
method as well and have an awesome new ruby-esque syntax for instantiation.
I'm ESPECIALLY unhappy that there's a semantic difference between implicit and explicit objects. That one, I cannot stand for.
Agreed.
Things are much simpler for Coco, where the magic occurs only on the top level naked function/object. So this Coco code
class C
->
{}
desugars to
function C ->
C:: import {}
mirroring the primitive JS idiom
function C(){}
C.prototype = {};
Problems I see with this syntax:
constructor
property was really just fine!@prop: value
to @prop = value
.I am happy with this feature being added if it doesn't change the behavior of my already written and working code.
Why create the extra bloat (closure wrapper) when the executable bodies aren't being used
Constructors will be wrapped regardless of this change due to JScript bugs. See #729.
@satyr: say what? We weren't wrapping before and everything worked just fine...
@devongovett On your third point: While backward compatibility is nice, @prop: value
was always weird—the one place in CoffeeScript where @
didn't mean this
. So, I'm glad to be rid of that syntax.
@michaelficarra Could you elaborate on your objections with specific examples?
@TrevorBurnham maybe I'm misunderstanding you, but switching from @prop: value
to @prop = value
doesn't really change anything about what @
refers to...
[Update: Reconsidered, see below.]
@michaelficarra Could you elaborate on your objections with specific examples?
@TrevorBurnham: I'm not quite sure on what you want me to elaborate. Can you quote specific statements I made?
Also, @devongovett: Coffeescript is still in alpha, meaning its syntax and semantics WILL change and cannot be relied upon. From the official documentation:
Disclaimer: CoffeeScript is just for fun. Until it reaches 1.0, there are no guarantees that the syntax won't change between versions.
It's unfortunate if you have to change code you already wrote (I'm in the same boat), but we are not looking to maintain backwards compatibility with any other alpha versions of CS. We should not take that point into account. To continue compiling your code the way it is, you can always make it rely on a specific (alpha) version of Coffeescript, but for right now, it's probably best to flow with the changes.
Uhhh, did several comments get deleted from this thread somehow...? The latest I'm seeing is michaelficarra's that concludes "it's probably best to flow with the changes."
Uhhh, did several comments get deleted
Yup. It is actually much worse than it sounds in that blog post as you can tell.
[As it happens, I did save the proposal I posted before the outage. Here it is again...]
OK, I've taken a more serious look at the new syntax, so let me take a step back. On the current master, a simple class can look like
class Model
doSomething()
count = 0
@staticFunc = -> 'foo'
instanceFunc: -> 'bar'
() -> console.log @instanceFunc() + ++count
There are several unintuitive things here, notably:
doSomething()
in the constructor? Weird. You essentially have two subtly different ways of doing constructor-y things, and you can mix the two freely.@staticFunc
is static, or why a reference from the constructor to @staticFunc
would fail (you would have to write Model.staticFunc
instead). Also, note that @staticFunc =
and @staticFunc:
are equivalent, setting the stage for annoying debates about which style is better.But I come to praise the new class syntax, not to bury it! Several interesting things are being done here; for instance, note that @
and this
compile to Model
within the class body. I think we can build on those things and come up with a clearer, yet equally powerful syntax:
Model.property = ...
) in the class body to use :
rather than =
(paralleling the public instance method syntax).@@
to the class name, and allow its use within class methods as well as the class body. Disallow use of @
/this
in the class body, as they'd just mean the same thing.With these changes in place, the class declaration from above would look like
class Model
count = 0
@@staticFunc: -> 'foo'
() ->
doSomething()
console.log @instanceFunc() + ++count
instanceFunc: -> 'bar'
And you'd be able to reference @@staticFunc
from the constructor as well as other functions.
These changes would preserve all of the power of the new syntax, while making CoffeeScript classes look and feel a lot more intuitive. To someone who understands JavaScript functions and has used objects in any other language, you would be able to explain CoffeeScript classes thusly:
class
is a special function declaration. Because it's a function, anything defined in the class body with =
is visible only within the class.(arguments...) -> ...
property: ...
Refer to these as @property
from within instance methods.@@
prefix, as in Ruby, and the hash syntax: @@property: ...
They can be referenced anywhere within the class via either @@property
or ClassName.property
.Thoughts?
Great proposal. Don't really care about not having to type constructor for a class' constructor. It doesn't bother me in Ruby/Python either.
Just to pipe back in with a ticket-that-was-erased-by-the-github-crash, here:
The new executable classes are backwards-compatible, syntactically, with existing classes. This means that @static: val
continues to work, and constructors are labeled with constructor:
...
I'm not sure if this ticket should have been closed just yet. There are still a lot of unanswered issues that many people have brought up. @jashkenas: can you take another read-through of this thread and make sure you're still comfortable closing it with things in their current state? There were a lot of really good points that I don't want ignored.
Yes -- gave the ticket another read through, and I am comfortable closing it in it's current state. Feel free to drop by #coffeescript
if you'd like to chat about particular bits. The main opposing proposition here is something similar to Stan's #640, which we're not going to do: it's a poor syntax relative to this one, and privileges "private" (local) properties over prototypal properties, which defeats the purpose of a class definition: to define a prototype. If you just want a list of local functions, use a closure.
The bit that I'm least comfortable with is the rules for converting key: val
assignments into prototypal properties, but at the moment, it's backwards-compatible with 0.9.4, and we can continue to refine it from there.
How about the difference I pointed out between implicit and explicit object syntax? There should never be a difference between those two. It's an extremely slippery slope, conveying to people that they are actually two different things, when in any other context they are not.
Also, there seemed to be no real consensus on how we should define the constructor, syntactically speaking. I'm fine with pretty much anything suggested (implicit constructor, constructor
special case, new
special case, etc.), but we should hear some arguments for and against these.
And lastly and probably most importantly, the method assignment notation: StanAngeloff and I especially were pushing for uses of @
, @@
, and =
so that class definitions were not so different from other coffeescript constructs.
Maybe these (and unlisted others) should all be made into separate issues, and discussion should continue there? I'm fine with pushing out another point release with the way things are now, but these should be discussed before 1.0. I'd be willing to start these issues tonight if nobody else takes it up before then.
The difference between implicit and explicit object literals is the piece of it I'm least comfortable with ... but I think it's the correct syntax here. Think of key: value
as assigning a property, and var = value
as assigning a variable. Here we're assigning properties of the class. Think of it like this, in terms of tokens:
{ key: value }
class key: value outdent
Constructors continue to be defined with constructor:
because none of the alternatives are demonstrably better, and this is backwards-compatible.
Prototype property assignment notation cannot use an @
symbol. The entire point of having a class definition is to facilitate the assignment of prototype properties -- we can't require them to be prefixed with this
-- especially when this
is not a reference to the prototype. It doesn't make sense semantically, and optimizes the syntax for the wrong thing.
jashkenas: I find your arguments entirely agreeable and, although I was worried about being too hasty with this issue, support your decision in closing it.
@jashkenas: care to give a reason for this re-opening?
Yep. @wycats wants to make the case for bringing back the extended
hook, so I re-opened this ticket for him.
I have a case for the extended
hook to return as well... So, here goes.
Basically, since properties on the constructor are copied over when extending but not deep copied, if you had an object or array property on the parent class, you get a reference to the same object on the extended class, which may not be the desired behavior. Here is an example, from a piece of my own code, as well as how I got around it:
I'm building an ORM in CoffeeScript, and on the constructor of each data model I have a fields object that stores information about each defined property for records of that type. Because all models extend from a base model class or from other models, when I extend, the fields object gets copied over as desired, but because it is a reference to the property on the parent class, any fields I add to the extended model get added to the parent model as well - definitely not desired. I fixed this by dynamically checking whether the fields object was equal to the one on the super class (using CoffeeScipt's __super__
property, and deep copy it if so. This was only possible because I have a method that gets called for every added field, but you can imagine a situation where that isn't the case.
There are a few possible fixes for this... If there was an extended
hook, I could run my deep copy ahead of time on those properties. The other option would be for CoffeeScript to do deep copies when extending classes, but this would be a larger change for the language...
So that's my case for the return of the extended
hook. I'm sure @wycats has a good one as well.
Ah, yes, that one. You could probably already tell, but I'm in favor of an extended
call as well.
I forked coffeescript to add an extended hook so I could write jQuery plugins as classes that extend a base class. It looks something like this (the ugly part is parsing the function name out of the constructor, but besides that I like it):
class NodeList
@extended: (base) ->
name = base.toString().match(/^\s*function\s+([^\s\(]+)/)[1]
if name
$.fn[name] = (options)->
@each (i, el) ->
$el = $(el)
return if $el.data(name)
instance = new base($el, options)
$el.data name, instance
class MyPlugin extends NodeList
constructor: (element, options) ->
console.log element, options
//now you can do $('.foo').MyPlugin({foo: 'bar'});
//and you get the added benefit of abstracting logic away from a jQuery specific implementation
@keithnorm: Instead of parsing the function name out, why don't you just pass it to the extended
hook? It's not like it's not available at compile time...
@michaelficarra: Since the extended hook is called from the __extends method, so that method should only know about a generic parent and child, it seems like the only way to do this would be to store the name as a property on each class. That sound right?
MyPlugin = (function() {
MyPlugin.name = 'MyPlugin';
__extends(MyPlugin, NodeList);
function MyPlugin(element, options) {
}
return MyPlugin;
})();
Some alternative solutions:
__extends
call (a little ugly)extended
call from __extends
to the class-defining IIFE (not that great)On every major engine but Trident, that name
property you define is already defined and not writable. So your solution would be best. Unfortunately, we already tried that approach in #1272, but @jashkenas shot it down.
Ok cool I can work around the name
issue for now, but as far as getting the extended hook merged in, what are the chances? I looked at your gist @michaelficarra and we have arrived at the same conclusion. What is the push back?
I'm going to me too a request for an extends hook. I'm currently patching my install of coffeescript to change __super
to superclass
so I can use the class...extends syntax with yui3.
I know it's been a while, but is anyone in here still hankering after the extended
hook ... or have y'all found manual registration to work just as well?
I've been sticking with 0.9.4
But there are so many nice things on the newer versions, that I'm thinking about forking it and adding extended hook back. And metaprogramming through plugin support back as well.
@danielribeiro: wow. I would have forked it long ago. It's not like it'd be hard to maintain. How often does that single line of code change? Almost never.
There's an
executable
branch over yonder:https://github.com/jashkenas/coffee-script/tree/executable
And if you're feeling brave, I'd recommend checking it out.
It pulls in parts of satyr's coco patch for executable class bodies, and tweaks some things. This means that:
@prop = val
(instead of the previous@prop: val
). This is becausethis
is a reference to the class (constructor function) during the course of the definition.constructor
property. A naked function at the top level serves as the constructor.extended
hook, and added in static inheritance, for reasons you'll see demonstrated shortly. This means that your sublass gets all of it's parent's static properties.A few examples... A regular class now looks like this:
Since it's arbitrary code, you can conditionally define methods.
What does that compile into, you may ask...
And finally, let's rustle up some metaprogramming. The following is part of a test case on
executable
. The idea is to create a base class that provides a simple way to define attributes, creating a function that serves as a getter and a setter for the private storage of a value.What do y'all think about this proposal?