Skip to content
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

What should super refer to? #3796

Closed
lydell opened this issue Jan 16, 2015 · 17 comments
Closed

What should super refer to? #3796

lydell opened this issue Jan 16, 2015 · 17 comments
Labels

Comments

@lydell
Copy link
Collaborator

lydell commented Jan 16, 2015

I know that super has been discussed many times before (#2638 and #3436 for example), but in my opinion this is something new. This issue is not about changing super totally, but merely about defining its edge cases. The reason I started this discussion is because of issues I bumped into when making #3785.

This issue is not about what super without parentheses should do etc. It is only about what super should refer to in different cases.

The only way I’ve ever used super, and the only way I’ve even seen super be used, is like this:

class A extends B
  method: -> super

That is, only in an instance method defined using the method: syntax inside a class body with extends.

If that was the only case super was allowed, compiling it would be trivial. The above could compile to:

var A;

A = (function(_super) {
  __extends(A, _super);

  function A() {
    return _super.apply(this, arguments);
  }

  A.prototype.method = function() {
    return _super.prototype.method.apply(this, arguments);
  };

})(B);

CoffeeScript also allows super in “static” methods:

class A extends B
  @method: -> super

That would also be easy to compile:

var A;

A = (function(_super) {
  __extends(A, _super);

  function A() {
    return _super.apply(this, arguments);
  }

  A.method = function() {
    return _super.method.apply(this, arguments);
  };

})(B);

If dynamic keys are implemented, that’d be straightforward too:

class A extends B
  "#{foo()}": -> super
var A;

A = (function(_super) {
  var _name;

  __extends(A, _super);

  function A() {
    return _super.apply(this, arguments);
  }

  A.prototype[_name = "" + foo()] = function() {
    return _super.prototype[_name].apply(this, arguments);
  };

})(B);

So far so good. All nice and simple.

CoffeeScript also allows a “classic” style for making “classes”:

A = ->
A extends B
A::method = -> super

Note that the above isn’t equivalent to the class examples, since the constructor A doesn’t apply B. That could be done by writing A = -> B.apply this, arguments manually, but it would be much nicer to be able to write A = -> super, but that throws an error.

There is no way to make a static method call super using the “classic” style. A.method = -> super throws an error.

So when is it possible to use super “classic” style?

A = -> super # throws
A.m = -> super # throws
A::m = -> super # works
A.prototype.m = -> super # works
A["prototype"].m = -> super # throws
A["pro" + "totype"].m = -> super #throws
A[prototype].m = -> super #throws

According to #1392 you should be able to namespace classes (properly implemented in #3785):

namespaced.A::m = -> super # works

So what’s the problem? First of all, we don’t know the superclass, unlike in the class examples above. Therefore, the superclass is assumed to be accessible at A.__super__.

  • A: Could be compiled to use A.__super__.constructor.

  • A.m: Could be compiled to use A.__super__.constructor.m.

  • A::m, A.prototype.m: No problems. If we allow the above though, it might be confusing that super refers to different things in A.Prototype.m and A.prototype.m.

  • A["prototype"].m: If A.prototype.m is allowed, shouldn’t this also be? Ok, we could do it.

  • A["pro" + "type"].m: If A["prototype"].m is allowed, shouldn’t this also be? Ough, I see where this is going.

  • A[prototype].m: If A["prototype"].m is allowed, I’d expect this to be allowed, too. However, we don’t know if prototype is "prototype". A[f()].m=->super could compile to:

    var _super, _ref;
    
    _super = (_ref = f()) === "prototype" ? A.__super__ : A.__super__.constructor;
    A[_ref].m = function() {
      _super.apply(this, arguments);
    };

    But perhaps that single-time runtime check is a bit weird. Also it feels odd that A[f()].m=->super does different things to super depending on the value of f(). Therefore it might be better to compile it to assume that it always means a definition of a static method. I mean, why would you ever use something else than A::m or A.prototype.m if you really wanted to create an instance method? There’s no reason to.

    But even if A[b].m always meant static method, should A["prototype"].m too? What about A["pro" + "type"].m?

There’s also the case where super is used in an object literal:

extend A,
  method: -> super

However, why use an extend library function when you’re using CoffeeScript and have extends? And shouldn’t that extend function also provide some alternative to super? So I guess this case should be disallowed, just like [1, (-> super), 2] is.

Summing up, this is what I think:

The A.__super__ thing is ugly.

super is not “classic”: It does not belong to the “classic” style of making classes. If you’re using the classic style you’re not using super—instead, you might use some manual/“classic” way of doing it.

So in my opinion, super should only be allowed in a function after a method: or @method: in a class body. Nowhere else. That’s where they belong, and that’s where they’re simple. That’s also the only place I’ve ever seen them used. Compile it like the first two examples. Keep it simple! And as far as I understand, super will only be available in class blocks in ES6 (I might be wrong, though).

If that proposal is not acceptable, I’d say that super should refer to:

A = -> super
  → A.__super__.constructor
A.method = -> super
  → A.__super__.constructor.method
A.prototype = -> super
  → A.__super__.constructor.prototype
A.prototype.method = -> super
  → A.__super__.method 
A.Prototype.method = -> super
  → A.Prototype.__super__.constructor.method
A[anything].method = -> super
  → A[anything].__super__.constructor.method
A["prototype"].method = -> super
  → A["prototype"].__super__.constructor.method
  A known “gotcha”. `[]` notation always means static method. CoffeeScript should not try to interpret the code inside the brackets.

Oh, and if anyone brings up ES6 super, as far as I understand super() calls the method with the same name of the superclass, while super.anySuperMethod() calls “anySuperMethod” of the superclass.

What are people’s thoughts?

@epidemian
Copy link
Contributor

The A.__super__ thing is ugly.

super is not “classic”: It does not belong to the “classic” style of making classes. If you’re using the classic style you’re not using super—instead, you might use some manual/“classic” way of doing it.

Totally agreed.

I also find it very inelegant that A::method = -> super or A.protoype.method = -> super work, while other seemingly equivalent forms don't:

# Extracting the prototype into a variable doesn't work:
proto = A.prototype
proto.foo = -> super # Error

# Nor does using a function to assign the methods to the prototype:
_.extend A.prototype,
  method: -> super # Error

That being said, however, couldn't we make expr().method = -> super or expr1()[expr2()] = -> super work in some way? Would compiling to something like this work?:

// For `expr().method = -> super`
var _ref, _super;
_super = (_ref = expr()).method
_ref.method = function() {
  _super.apply(this, arguments)
};

// For the more general `expr1()[expr2()] = -> super` case:
var _ref1, _ref2, _super;
_super = (_ref1 = expr1())[_ref2 = expr2()];
_ref1[_ref2] = function() {
  _super.apply(this, arguments)
};

@lydell
Copy link
Collaborator Author

lydell commented Jan 18, 2015

That will probably raise a"TypeError: Cannot call method 'apply' of undefined", wouldn't it?

Edit: Just checked, it does.

@epidemian
Copy link
Contributor

@lydell, this is a little example of what i meant: http://jsfiddle.net/18nqo06h/

// class A
//   foo: -> console.log 42
var A = function() {};
A.prototype.foo = function() { console.log(42); }

// class B extends A
var B = function() {};
B.prototype = new A;

var f = function() { return B.prototype; };
var g = function() { return 'foo'; };

// f()[g()] = -> super
var _ref1, _ref2, _super;
_super = (_ref1 = f())[_ref2 = g()];
_ref1[_ref2] = function() {
  return _super.apply(this, arguments)
};

new B().foo() // 42

It has its caveats though. For instance, if A::foo was not defined at the time B::foo is defined, it would break; or if A::foo was changed after B::foo was defined, then B::foo would still call the original version of A::foo when doing super. But maybe these could be reasonable/expectable caveats of calling super outside a class. What do you say?

@lydell
Copy link
Collaborator Author

lydell commented Jan 18, 2015

Now I understand. Clever!

@epidemian
Copy link
Contributor

I now realize that it is still a very limited solution:

# Assigning the super-calling function to a variable wouldn't work:
f = -> super
A::method = f

# Nor would using a super-calling function as a method on a literal object: 
_.extend A.prototype, 
  method: -> super

So, yes, i would vote in favour of limiting the super syntax to methods inside a class statement.

Does ES6 allow super calls outside classes? On standalone functions or in object literals?

Does this "work" in ES6?

var a = {
  foo() { super() }
};

Object.setPrototypeOf(a, {
  foo() { console.log(42); }
});

a.foo() // 42?

(It does not compile on 6to5.org, but i don't know if that's per spec or not; something similar using a class does work)

@lydell
Copy link
Collaborator Author

lydell commented Jan 18, 2015

The only way I’ve ever used super, and the only way I’ve even seen super be used, is like this:

class A extends B
  method: -> super

Is there anyone who is a pro at searching Github and could back up this? If so, the decision to limit super to this case would be easier.

@vendethiel
Copy link
Collaborator

@lydell
Copy link
Collaborator Author

lydell commented Jan 19, 2015

Looking through the first ten pages as well as a few of the last and a bunch in the middle of those search results, I found one occasion where super wasn't used like above, but that one was a mistake by the developer.

@jashkenas
Copy link
Owner

In general the current idea is to mostly limit super use to methods within class bodies — and indeed you note that this is how it is almost always used. We're never going to be able to completely statically analyze calls that are constructed in crazy ways, and we shouldn't try.

Can you fill me in on exactly what situations we would lose support for if we followed @lydell's "keep it simple" suggestion?

@lydell
Copy link
Collaborator Author

lydell commented Jan 29, 2015

The following uses of super would be parse errors:

class A
  m: -> super # missing `extends`
  @m: -> super # ditto
  @::m = -> super # regular assignment not allowed
  @m = -> super # ditto

A::m = -> super # ditto
A.prototype.m = -> super # ditto

This is would continue to be allowed:

class A extends B
  m: -> super
  @m: -> super

Edit: Since #3785 has been merged the new cases allowed by that PR would become parse errors, too.

@jashkenas
Copy link
Owner

And that would allow us to remove the __super__ property entirely?

@lydell
Copy link
Collaborator Author

lydell commented Jan 29, 2015

Yep (see the beginning of the first post).

@jashkenas
Copy link
Owner

I'm very tempted — so much so that I started typing out a "go for it" response before changing my mind — but I don't think that we should make that change. The main reason is this case:

class A extends B
  ... blah blah ...

... and then later, in another file, say a-extensions.coffee:

A::method ->
  super
  ... continue refining method ...

Removing the __super__ property would be more elegant, but it would prevent CoffeeScript classes from being "open" except in the same lexical scope where they're first defined. It's not the most killer restriction, but it's nice that you can still add methods to them outside of that scope, and call super like normal.

@lydell
Copy link
Collaborator Author

lydell commented Jan 29, 2015

Why not write

A::method = ->
  B::method.call this
  ... continue refining method ...

in that case? Besides, have you ever done that, or seen anyone do it?

@jashkenas
Copy link
Owner

Because you don't want to have to know or think about what the name of the superclass happens to be. It could even be a value that's set at runtime in the other class, and you can't know what the name is. (Contrived, but possible.)

I haven't ever done that, but I don't write much class-based CoffeeScript ;)

@lydell
Copy link
Collaborator Author

lydell commented Jan 30, 2015

What about this then? (Inspired by @epidemian above.)

superMethod = A::method
A::method = ->
  superMethod.call this
  ... continue refining method ...

Pretty nice, eh?

But anyway, I can’t believe that you argue for keeping the current somewhat odd semantics to support a non-use case! :)

@lydell
Copy link
Collaborator Author

lydell commented Aug 28, 2015

JavaScript has one super, CoffeeScript has another. I’m basically suggesting a third one here, which is one too much, especially considering the low number of responses. Closing.

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

No branches or pull requests

4 participants