Extended/Included/Initialized Callback? #1883

Closed
lancejpollard opened this Issue Nov 20, 2011 · 3 comments

3 participants

@lancejpollard

I saw this issue was closed:

#867

Having something like that would solve the following edge case:

When you have ParentClass and ChildClass extends ParentClass, and you have a "class attribute" on ParentClass, changing the value of that class attribute on either ParentClass or ChildClass will effect both. This is only for arrays and objects it seems.

I see how that's expected behavior, but I want to be able to initialize a class variable for each sub class. Can I already do that with CoffeeScript?

(for all the code below, assume @extend is implemented like this [from Spine.js]):

specialProperties = ['included', 'extended', 'prototype']

class Base
  @extend: (object) ->
    for key, value of object when key not in specialProperties
      @[key] = value
    @

Here's the example showing what currently happens:

SomeModule =
  overriddenClassProperty: ["I'll be manually overridden in subclass"]
  nonOverriddenClassProperty: ["I won't be overridden!"]

class User extends Base
  @extend SomeModule

class Admin extends User
  @overriddenClassProperty: ["been overridden"]

User.nonOverriddenClassProperty.push "I'm the same..."
Admin.nonOverriddenClassProperty.push "Still the same..."

console.log User.name + ": " + User.overriddenClassProperty.join(" ")
# User: I'll be manually overridden in subclass
console.log Admin.name + ": " + Admin.overriddenClassProperty.join(" ")
# Admin: been overridden
console.log User.name + ": " + User.nonOverriddenClassProperty.join(" ")
# User: I won't be overridden! I'm the same... Still the same...
console.log Admin.name + ": " + Admin.nonOverriddenClassProperty.join(" ")
# Admin: I won't be overridden! I'm the same... Still the same…

Here's what I'd like to be able to do:

specialProperties = ['included', 'extended', 'prototype']

class Base
  @extend: (object) ->
    for key, value of object when key not in specialProperties
      @[key] = value

    if extended = object.extended
      @extendedCallbacks.push extended
      extended.apply(@)

    @

  # something like this built in...
  @extended: ->
    for callback in @extendedCallbacks
      callback.apply(@)

SomeModule =
  overriddenClassProperty: ["I'll be manually overridden in subclass"]

  extended: ->
    @extendedOverriddenClassProperty = ["Awesome! I was overridden!"]

class User extends Base
  @extend SomeModule

class Admin extends User
  @overriddenClassProperty: ["been overridden"]

User.extendedOverriddenClassProperty.push "I'm for the User..."
Admin.extendedOverriddenClassProperty.push "I'm for the Admin..."

console.log User.name + ": " + User.overriddenClassProperty.join(" ")
# User: I'll be manually overridden in subclass
console.log Admin.name + ": " + Admin.overriddenClassProperty.join(" ")
# Admin: been overridden
console.log User.name + ": " + User.extendedOverriddenClassProperty.join(" ")
# User: Awesome! I was overridden! I'm for the User
console.log Admin.name + ": " + Admin.extendedOverriddenClassProperty.join(" ")
# Admin: Awesome! I was overridden! I'm for the Admin

Does this exist, or is this something that could be added?

Currently, the only way around this is to wrap class variables like this in functions:

SomeModule =
  someClassAttribute: ->
    @_someClassAttribute ||= []

Adding a built in extended hook something like above would make it so I could write them as:

SomeModule =
  someClassAttribute: []

…which, more importantly, I can invoke them as SomeClass.someClassAttribute instead of SomeClass.someClassAttribute(), which provides the following benefits:

  1. Simplifies the API on the class
  2. Improves performance (however slightly) because you're not invoking a function, and you're only creating one attribute instead of two.
  3. Makes inspecting the attributes in the web inspector/console much easier, I see someClassAttribute: ["item a"] instead of someClassAttribute: function () { … and somewhere else in the prototype chain _someClassAttribute: ["item a"].

Initialized Callback

It would also be very helpful to have an initialized callback that the compiler injects into the constructor of every class. This way you could write "modules" and have them append code to the constructor without having to mess with the prototype chain. If you could solve it so it added the module to the prototype chain, that would be amazing, but the following would work as well.

AttributeChangesModule =
  initialize: ->
    @changes = {}

class User
  constructor: (attr = {}) ->
    @attributes = {}
    @attributes[key] = value for key, value of attr

class Admin extends User
  @extend AttributeChangesModule

# so you could use it like this:
user = new User
console.log user.changes #=> undefined

admin = new Admin
console.log admin.changes #=> {}

The only way to do this would be to change the generated code:

// currently generated code
var Admin, User;

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

User = (function() {
  function User() {}
  return User;
})();

Admin = (function() {
  __extends(Admin, User);
  function Admin() {
    Admin.__super__.constructor.apply(this, arguments);
  }
  return Admin;
})();
// proposed generated code
// has hooks for `extended` and `initialized`
var Admin, User;

var __hasProp = Object.prototype.hasOwnProperty;
var __extends = function (child, parent) {
  for (var key in parent) {
    if (__hasProp.call(parent, key)) child[key] = parent[key];
  }

  // extended callback
  if (typeof(parent.extended) == "function")
    parent.extended.call(child)

  function ctor() {
    this.constructor = child;
  }

  ctor.prototype = parent.prototype;
  child.prototype = new ctor;
  child.__super__ = parent.prototype;
  return child;
};

User = (function() {
  function User() {}
  return User;
})();

Admin = (function() {
  __extends(Admin, User);
  function Admin() {
    Admin.__super__.constructor.apply(this, arguments);
    if (User.initialized)
      User.initialized.apply(this, arguments)
  }
  return Admin;
})();

A complete, robust solution would be having the super implementation closer to Resig's Class implementation.

http://ejohn.org/blog/simple-javascript-inheritance/

Using that implementation, the generated code could leave out the parent.extended.call(child) code, and you could simply call _super? on ANY method. If you added 3 modules to a class, each of which had a method like someMethod: -> super?, and in the class you called someMethod: -> super, it would call that method on all of the modules. That is not possible in coffeescript right now, from my knowledge.

Here's the comparison to CoffeeScript:

// coffeescript's generated code for copying properties
for (var key in parent) {
  if (__hasProp.call(parent, key)) child[key] = parent[key];
}

// a more robust version, from resig
for (var name in prop) {
  // Check if we're overwriting an existing function
  prototype[name] = typeof prop[name] == "function" && 
    typeof _super[name] == "function" && fnTest.test(prop[name]) ?
    (function(name, fn){
      return function() {
        var tmp = this._super;

        // Add a new ._super() method that is the same method
        // but on the super-class
        this._super = _super[name];

        // The method only need to be bound temporarily, so we
        // remove it when we're done executing
        var ret = fn.apply(this, arguments);        
        this._super = tmp;

        return ret;
      };
    })(name, prop[name]) :
    prop[name];
}

By having that implementation, if you extend some class with 3 modules, all of which have an extended and included function, you could call super conditionally.

ModuleA =
  extended: ->
    # if (this._super != null)
    #   this._super.apply(this, arguments)
    super?
    console.log "ModuleA!"

ModuleB =
  extended: ->
    # this._super.apply(this, arguments)
    super
    console.log "ModuleB!"

class User extends Base
  @extend ModuleA
  @extend ModuleB

User.extended()
# "ModuleA!"
# "ModuleB!"

Likewise, if this was injected into the constructor function, adding to the constructor function would be EASY from modules:

class Base
  constructor: ->
    console.log "Base initialized!"

ModuleA =
  initialize: ->
    super?
    console.log "ModuleA initialized!"

ModuleB =
  initialize: ->
    super
    console.log "ModuleB initialized!"

class User extends Base
  @extend ModuleA
  @extend ModuleB

  constructor: ->
    super
    console.log "User initialized!"

user = new User
# "Base initialized!"
# "User initialized!"
# "ModuleA initialized!"
# "ModuleB initialized!"

Having that would help so much. Specifically, it is currently impossible to extend the inheritance chain using CoffeeScript's class construct, unless you call super somewhere:

With this code below, I have no way of extending the User constructor with composition

class User
class Admin extends User

because the generated code has no hooks:

User = (function() {
  function User() {} // nothing to hook into
  return User;
})();

Admin = (function() {
  __extends(Admin, User);
  function Admin() {
    Admin.__super__.constructor.apply(this, arguments);
  }
  return Admin;
})();

A hack I've been having to use requires writing modules as functions instead of objects, implementing a custom @extend class method that's basically like __extends, and calling the constructor function with super in all of them:

class SomeModule
  constructor: -> super

class User extends Base
  # __extend User, SomeModule
  @extend SomeModule

class Admin extends User

As you can see, to make that work right, I have to modify the prototype chain in ways I haven't even fully figured out how to do correctly. But, doing it like this is the only workaround I've found -- making sure TheClass.__super__.constructor.apply(this, arguments); is called in every module.

Implementing it something like John Resig's version seems like it would solve all of these problems. What do you think? I know that was a crazy long post, but this is the one thing about CoffeeScript that's really missing to enable full power. I feel like there's a huge wall of stuff I can't do in CoffeeScript without this.

@lancejpollard

As a side note, I don't feel like this is anything like syntactic sugar or the things discussed on the FAQ.

https://github.com/jashkenas/coffee-script/wiki/FAQ

I'm totally cool with implementing my own extended method that implements module functionality, that makes sense. It's just that not having a hook in the constructor, and not having super call something like this._super instead of HardcodedClass.__super__, makes it so I can't even use the Composition pattern in any real way beyond copying properties from one object to another. Even if I override/customize the hidden/generated __extends method, I still don't have the ability to add an initialized hook into the constructor. Hopefully this type of thing is something you guys are interested in adding.

@showell

See #841.

@vendethiel
Collaborator

Closing since extended hook has been removed

@vendethiel vendethiel closed this Mar 25, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment