Skip to content

Class system extendability - A simple solution #1762

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

Closed
ghost opened this issue Oct 10, 2011 · 31 comments
Closed

Class system extendability - A simple solution #1762

ghost opened this issue Oct 10, 2011 · 31 comments

Comments

@ghost
Copy link

ghost commented Oct 10, 2011

Hi,

The one thing keeping me from using CoffeeScript full-time is that it's class system cannot be extended in any way. Below is an idea that allows users to hook into the class system, it's very similar to that in Ruby and Spine.js.

I am suggesting that the following coffee script:

class Bar

class Foo extends Bar
  myInstanceMethod: -> console.log 'I am an instance method';
  @myClassMethod: -> console.log 'I am a class method';

Should generate:

var Bar, Foo;
var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
  var mixin = function(target, obj) {
    for (var key in obj) { if (__hasProp.call(obj, key)) target[key] = obj[key]; } };
  mixin(child, parent);  
  function ctor() { this.constructor = child; }
  ctor.prototype = parent.prototype;
  child.prototype = new ctor;
  child.__super__ = parent.prototype;
  child.include = child.include || function(obj) { mixin(this.prototype, obj); };
  child.extend = child.extend || function(obj) { mixin(this, obj); };
  if (parent.inherited) parent.inherited(child);
  return child;
};
Bar = (function() {
  function Bar() {}
  return Bar;
})();
Foo = (function() {
  __extends(Foo, Bar);
  function Foo() {
    Foo.__super__.constructor.apply(this, arguments);
  }
  Foo.include({
    myInstanceMethod: function() {
      return console.log('I am an instance method');
    }
  });
  Foo.extend({
    myClassMethod: function() {
      return console.log('I am a class method');
    }
  });
  return Foo;
})();

This way, should Bar want to hook into the class system it could do the following:

class Bar
  @include: (obj) ->
    console.log "About to declare: '#{key}' on proto" for key in obj
    super

  @extend: (obj) ->
    console.log "About to declare: '#{key}' on class" for key in obj
    super  

class Foo extends Bar
  myInstanceMethod: -> console.log 'I am an instance method';
  @myClassMethod: -> console.log 'I am a class method';

I think this would make CoffeeScript's class system truly powerful and allow things like Ext's preprocessors... the applications are endless. The other benefit of this is that the resulting minified JS would be smaller due to the object notation used in include and extend. Obviously the include and extend function names are just a suggestion but I think the likeness to Ruby is a good thing. To avoid conflicts with existing code, they could always be __include and __exclude.

This tiny change also introduces Ruby-esque 'Modules' e.g.

CommonInstanceMethods =
  sharedFunction -> console.log 'I come from CommonInstanceMethods'

CommonClassMethods =
  sharedFunction -> console.log 'I come from CommonClassMethods'

class Foo
  @include CommonInstanceMethods
  @extend CommonClassMethods

class Bar
  @include CommonInstanceMethods
  @extend CommonClassMethods

I would create a patch but don't know enough about parsers to be able to implement it. I really hope we can add this as currently there is no way to extend the class system.

Thanks,

Jamie

@ghost
Copy link
Author

ghost commented Oct 10, 2011

One more thing... for ultimate flexibility the __extends function could call if (parent.inherited) parent.inherited(child) before returning child. This would allow a class to know when it has been inherited, again similar to Ruby.

@michaelficarra
Copy link
Collaborator

This doesn't necessarily need to be baked into the language. See https://github.com/jashkenas/coffee-script/wiki/Mixins for a quick and dirty example with similar goals. I do kind of like your suggestion, but I don't really like how it complicates coffeescript's extends semantics. Still, I give a very weak +1 for now.

@michaelficarra
Copy link
Collaborator

for ultimate flexibility the __extends function could call if (parent.inherited) parent.inherited(child) before returning child

@soniciq: we used to have this feature, in the form of the extended method. See #516 and #814, which is open right now specifically to bring it back. We could kill two birds with one commit right here.

@ghost
Copy link
Author

ghost commented Oct 10, 2011

@michaelficarra The Mixin example is possible however if your classes need to introspect property definitions etc, there is currently no way to achieve this.

I see what you mean about not wanting to complicate things but to be honest I actually think it makes thinks simpler... for the few extra bytes of additional output you get the ability to include instance properties, the ability to extend classes, the ability to extend the class system itself and smaller files when minified.

Here is an updated __extends function to include the 'inherited' callback:

var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
  var mixin = function(target, obj) {
    for (var key in obj) { if (__hasProp.call(obj, key)) target[key] = obj[key]; } };
  mixin(child, parent);
  function ctor() { this.constructor = child; }
  ctor.prototype = parent.prototype;
  child.prototype = new ctor;
  child.__super__ = parent.prototype;
  child.include = child.include || function(obj) { mixin(this.prototype, obj); };
  child.extend = child.extend || function(obj) { mixin(this, obj); };
  if (parent.inherited) parent.inherited(child);
  return child;
};

Update: Also update original code.

@maccman
Copy link

maccman commented Oct 10, 2011

Obviously a +1 from me :) - this would be awesome!

Would also help people compartmentalize and DRY their code by encouraging modules. It's a small change, but could have a big impact on how people write their CoffeScript.

@aemadrid
Copy link

+1 - seems a good trade-off.

@benjreinhart
Copy link

I think this is a great idea. I think it can be misleading when using the class keyword when really what I'm trying to do is modularize code, but yet I'm not planning on instantiating the class.

I think if this is done, it would be really cool to add a module keyword, just like ruby, which would be nice and readable.

@mythz
Copy link

mythz commented Oct 10, 2011

Seems weird to overload '@' to mean class, In all other usages I thought it meant 'this', i.e. references the instance? What about 'class.' or the name of the Constructor i.e. Foo?

class Bar

class Foo extends Bar
  myInstanceMethod: -> console.log 'I am an instance method';
  class.myClassMethod: -> console.log 'I am a class method';
  Foo.myClassMethod: -> console.log 'I am a class method';

@michaelficarra
Copy link
Collaborator

@mythz: that has nothing to do with this issue. The context of a class body is the class itself, not the instance. I don't particularly agree with it, but it's the way it is. The class name can also be used, as you suggested. This issue proposes adding the include and extend methods to the constructors of classes that extend other classes.

Now that I think about it, classes won't have include and extend methods unless they're extending a superclass. @soniciq: how do you propose we overcome this inconsistency?

@ghost
Copy link
Author

ghost commented Oct 10, 2011

@mythz This is how CoffeeScript works currently, the '@' is a shortcut to 'this.'.

@ALL Right, now we have a few +1's, does anyone know how to go about getting this into CoffeeScript. I've had a look at the code with a view to implement the change but it's all scary parser stuff and I'm not really sure where to start... I will continue to investigate but if anyone else wants to chip in, please do.

@maccman
Copy link

maccman commented Oct 10, 2011

@soniciq: @michaelficarra raises a good point, which needs to be addressed before we can go any further. If the class doesn't extend another class, than all that extra super and inheritance code won't be generated - not sure how we get round that.

@ghost
Copy link
Author

ghost commented Oct 10, 2011

@michaelficarra and @maccman Sorry missed your comment while writing my last one. That is a good point, I think I have a solution though... give me 15 to whip it up.

@ghost
Copy link
Author

ghost commented Oct 10, 2011

Right, here is a basic implementation, I'm working on cleaning it up a little. Note that the include and extend assignment only happens for classes that don't inherit another class as they will be copied by __extends to subclasses.

For the CoffeeScript:

class Foo
  myInstanceMethod: -> console.log 'Instance method'
  @myClassMethod: -> console.log 'Class method'

class Bar extends Foo
  myInstanceMethod: -> console.log 'Instance method'
  @myClassMethod: -> console.log 'Class method'

The following would be generated (you can copy this into a js console and include/extend Foo and Bar ):

var Bar, Foo;
var __hasProp = Object.prototype.hasOwnProperty,
    __mix = function(target, obj) {
      for (var key in obj) { if (__hasProp.call(obj, key)) target[key] = obj[key]; } },
    __extends = function(child, parent) {
  __mix(child, parent);
  function ctor() { this.constructor = child; }
  ctor.prototype = parent.prototype;
  child.prototype = new ctor;
  child.__super__ = parent.prototype;
  if (parent.inherited) parent.inherited(child);
  return child;
};
Foo = (function() {
  function Foo() {}
  Foo.include = function(obj) { __mix(this.prototype, obj); };
  Foo.extend = function(obj) { __mix(this, obj); };
  Foo.include({
    myInstanceMethod: function() {
      return console.log('Instance method');
    }
  })
  Foo.extend({
    myClassMethod: function() {
      return console.log('Class method');
    }
  });
  return Foo;
})();
Bar = (function() {
  __extends(Bar, Foo);
  function Bar() {
    Bar.__super__.constructor.apply(this, arguments);
  }
  Bar.include({
    myInstanceMethod: function() {
      return console.log('Instance method');
    }
  })
  Bar.extend({
    myClassMethod: function() {
      return console.log('Class method');
    }
  });
  return Bar;
})();

@michaelficarra
Copy link
Collaborator

A little problem:

class Foo
  @include: ->
  oops: ->

You don't need to compile to Foo.include or Foo.extend in the constructor, just compile to Foo.prototype.X = and Foo.X = and you won't have that problem. I'm going to be a weak -1 on this, though, since I don't want those methods cluttering up every constructor.

@ghost
Copy link
Author

ghost commented Oct 10, 2011

@michaelficarra compiling to Foo.prototype.X = and Foo.X = defeats the object as you then still can't intercept to property definition if you need to. As for cluttering up the constructor, I see what you mean but the benefit is huge... Ruby has had include and extend forever and they've not gotten in the way.

@ghost
Copy link
Author

ghost commented Oct 10, 2011

How about this: https://gist.github.com/1276838

@chrisjacob
Copy link

+1 for more investigation and discussion... I'm new to CoffeeScript - but I like the idea of modularising code. The resulting JS is pretty epic - but I suppose when you're trying to do a Class based style your going to get pretty complex JS output. Good luck!

@michaelficarra
Copy link
Collaborator

@soniciq: I thought the proposal was to only use include and extend methods when they were given explicitly:

class Foo
  @include someObject # just a regular method call
  someMethod: -> # define using `Foo.prototype.someMethod =`

I don't see what the benefit would be of passing someMethod through the include class method. Also, you didn't say how you would fix this problematic case:

class Foo
  @include: ->
  notGoingToBeDefined: ->

@showell
Copy link

showell commented Oct 10, 2011

FWIW you can kind of roll your own classes in CS already:

build_class = (proto, superclass)->
  # The class mechanism is mostly handled through JS, rather than
  # simulated, but we need to do this to play nice with JS libraries.
  extendify = (child, parent) ->
    ctor = ->
      this.constructor = child
      null # super important
    for key of parent
      if Object::hasOwnProperty.call(parent, key)
        child[key] = parent[key]
    ctor.prototype = parent.prototype
    child.prototype = new ctor
    child.__super__ = parent.prototype
    child

  X = ->
    this.__super__ = X.__super__
    if Object::hasOwnProperty.call(proto, "constructor")
      # debug "in constructor"
      proto.constructor.apply this, arguments
    else if superclass
      X.__super__.constructor.apply this, arguments
    else
      undefined
  if superclass
    extendify(X, superclass)
  for key of proto
    X.prototype[key] = proto[key]
  X

newify = (func, args) ->
  ctor = ->
  ctor.prototype = func.prototype
  child = new ctor
  result = func.apply child, args
  if typeof result is "object"
    result
  else
    child

class Bar
  yo: -> console.log "yo"

methods =
  constructor: (@foo) ->
  debug: -> console.log @foo
Foo = build_class(methods, Bar)

foo = newify Foo, [42]
foo.debug()
foo.yo()

If you want to experiment with class extension mechanisms without jumping right into the CS parser itself, the above code might be useful.

@ghost
Copy link
Author

ghost commented Oct 10, 2011

@michaelficarra I envisaged include and extend to be used by the generated code also. That way you could hook into them if desired e.g.

class Foo
  @include: (obj) ->
    console.log "Intercepted '#{key}' and did something clever" for key in obj
    super

class Bar extends Foo
  myProp: 20

This would output: Intercepted 'myProp' and did something clever. This is the flexibility that's missing from CoffeeScript... you can easily implement include and extend in a base class, as Spine.js does but only by having them at CoffeeScript's core can you really hook into class property definition etc. as above.

As for the problem case, I don't really see it as a problem as long as it's documented what include and extend do, just as you wouldn't try to redefine 'join' on a String object unless you had a very good reason to.

My Gist is currently work in progress and won't take into account the above 'super' call, I think the best way to candle this is with a super simple base constructor which I am looking into.

@ghost
Copy link
Author

ghost commented Oct 11, 2011

Right, I think I have it guys. This new Gist implements a super-super-simple base class which makes everything just work including the code in my previous comment: https://gist.github.com/1276838

@ghost
Copy link
Author

ghost commented Oct 11, 2011

@showell I was hoping to get something committed to CoffeScript to save having to jump through hoops like this.

@michaelficarra
Copy link
Collaborator

@soniciq: I like it. It's a much better solution. As to whether it should be included with CoffeeScript, I'm neutral. I don't see myself using the mixin pattern much, so if anything, I would default to keeping the compilation simpler. But the extended (or inherited in your example) hook is a very welcome enhancement. It should have never been removed in the first place.

@ghost
Copy link
Author

ghost commented Oct 11, 2011

Here's a jsFiddle if anyone wants a play: http://jsfiddle.net/A7PxS/

I am convinced this is the way to go, the added benefit you get from the additional 5 or 6 lines is immense, besides, by the time you have more than a couple of instance methods in your class, the total file size will be less than without this addition anyway as there's not MyClass.prototype.myFunc = littered everywhere, just myFunc:.

@chrisjacob
Copy link

Thx for the fiddle - excellent work from what I can see/understand. Some less technical feedback.... before jumping to "I am convinced" take some time away from your desk. Stop thinking about the problem/solution. Then come back with a clear head and re-evaluate your work... Sounds a bit redundant, but I know when I "sleep on it" I general change my opinion the next day. Just a thought. Again, great work.

@ghost
Copy link
Author

ghost commented Oct 11, 2011

@chrisjacob You are right, I was getting a little obsessive about this... I have slept on it now :)

I decided to think about other use-cases to give some perspective. Correct me if I'm wrong but I think this also helps with #1604 as you could just override include and extend to perform deep-copies of the properties.

I did think about attacking this at a slightly lower level, for example having defineProtoProperty and defineClassProperty functions would also allow you to hook into method definition and possibly make things a little easier for the compiler but the downside is verbose method declarations e.g.

Foo.defineProtoProperty('myInstanceMethod', function() {
  return 'I am an instance method';
});
Foo.defineClassProperty('myClassMethod', function() {
  return 'I am an class method';
});

This would again use a super simple base class like the one in the previous Gist and would make it really easy to extend the class system. The method names I've used here are probably too verbose but I've used them so it is clear what is going on.

Full implementation here: https://gist.github.com/1278710

What are peoples thoughts on the two options? Just chip in if I'm heading off course:

Option 1: https://gist.github.com/1276838
Option 2: https://gist.github.com/1278710

I think we definitely need an inherited callback in any case...

@pangloss
Copy link

pangloss commented Nov 6, 2011

+1 Has anyone followed up on this? I'd love to be able to use @include in my own project.

@eirikurn
Copy link

eirikurn commented Nov 6, 2011

+1 In most of my projects I have wanted to use the elegant class syntax but with a few extra hooks or custom features. The needs vary by project, but most of them could be fulfilled by these proposals.

@ghost
Copy link
Author

ghost commented Nov 6, 2011

@pangloss @eirikurn I've not had time to create a patch for this, to be honest I'm not entirely sure how to go about adding it as compilers scare me a little. Hopefully I'll get some time this week to write a patch.

@josher19
Copy link
Contributor

josher19 commented Jun 5, 2012

@soniciq
You might take a look at Chapter 3 of The Little Book on CoffeeScript:
http://arcturo.github.com/library/coffeescript/03_classes.html
for another way to do this.

@vendethiel
Copy link
Collaborator

Closing since the extended hook has been removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests