Leaky abstraction in dynamically assigned constructor functions #1966

Closed
jnicklas opened this Issue Dec 23, 2011 · 6 comments

Comments

Projects
None yet
5 participants
@jnicklas

So to my delight, I discovered that CoffeeScript does handle the case of assigning a function to the magic constructor property on CoffeeScript classes. However, I discovered that this is somewhat of a leaky abstraction.

In JavaScript, the return value of the constructor function matters. If an explicit return is used and that return returns an object (as opposed to a a "primitive" such as a string), then that object is returned. When a function is given explicitly to the constructor property, this works as expected in CoffeeScript, however, when the function is dynamically assigned, it does not.

This is an example:

class Foo
  constructor: ->
    return { foo: 'foo' }

class Bar
  ctor = -> return { foo: 'foo' }
  constructor: ctor

new Foo().foo # => "foo"
new Bar().foo # => undefined

Here we would expect both new Foo() and new Bar() to return the same result, but they do not.

I spent some time thinking about this, and my thinking is that given the constraints of CoffeeScript, this problem is impossible to fix, and I will probably just have to live with it. I realize this is a pretty weird edge case. Still wanted to write it down here in case it's of interest to anyone.

@michaelficarra

This comment has been minimized.

Show comment Hide comment
@michaelficarra

michaelficarra Dec 24, 2011

Collaborator

Well, it's consistent with the way constructors don't implicitly return, but I think the change you suggest provides a better consistency. I'll look into a patch.

Collaborator

michaelficarra commented Dec 24, 2011

Well, it's consistent with the way constructors don't implicitly return, but I think the change you suggest provides a better consistency. I'll look into a patch.

michaelficarra added a commit to michaelficarra/coffee-script that referenced this issue Dec 24, 2011

@michaelficarra

This comment has been minimized.

Show comment Hide comment
@michaelficarra

michaelficarra Dec 24, 2011

Collaborator
Collaborator

michaelficarra commented Dec 24, 2011

@shesek

This comment has been minimized.

Show comment Hide comment
@shesek

shesek Dec 24, 2011

Doesn't that mean that now class Foo then constructor: -> a:1 and fn = -> a: 1; class Foo then constructor: fn are inconsistent? (the latter would return the object, the first won't?)

shesek commented Dec 24, 2011

Doesn't that mean that now class Foo then constructor: -> a:1 and fn = -> a: 1; class Foo then constructor: fn are inconsistent? (the latter would return the object, the first won't?)

@jnicklas

This comment has been minimized.

Show comment Hide comment
@jnicklas

jnicklas Dec 25, 2011

I found another one of these inconsistencies, the same problem occurs when inheriting from another class as well:

class Foo
  constructor: ->
    return a: 'b'

class Bar extends Foo

new Foo().a # => 'b'
new Bar().a # => undefined

Again we would expect both statements to return the same result, but the first one returns undefined, and the second one doesn't.

I found another one of these inconsistencies, the same problem occurs when inheriting from another class as well:

class Foo
  constructor: ->
    return a: 'b'

class Bar extends Foo

new Foo().a # => 'b'
new Bar().a # => undefined

Again we would expect both statements to return the same result, but the first one returns undefined, and the second one doesn't.

@michaelficarra

This comment has been minimized.

Show comment Hide comment
@michaelficarra

michaelficarra Dec 26, 2011

Collaborator

@jnicklas: #1970 also fixes that behaviour. It's the same problem. Well, same fix at least.

Collaborator

michaelficarra commented Dec 26, 2011

@jnicklas: #1970 also fixes that behaviour. It's the same problem. Well, same fix at least.

@ich

This comment has been minimized.

Show comment Hide comment
@ich

ich Feb 23, 2012

I think it's not clear.

In topic message with return and externalCtor it's normal, cause you know what you do and expect to get other object, not instance of the class.

But when class extends other class:

  class MyObject extends Object # or RegExp, or Function, or Array
    method: ->

  new MyObject().method()

runtime error appears: TypeError: (new MyObject).method is not a function, cause new MyObject().method is undefined, cause constructor returns MyObject.__super__.constructor.apply(this, arguments); and its just an object with no methods, cause Object.prototype.constructor.apply({}) (MyObject.__super__ is Object.prototype) returns object.

It's really unexpected! What if I'd like to make extended Array class? I should call super (оr not):

class MyObject extends Array
  constructor: () ->
    super
    @push arguments...

  trace: -> this

console.log new MyObject(4,5).method() # [4, 5]

What's a point? I think it's gonna be better to describe this case in documentation: when your class extends other and constructor of other class returns custom object, you should manually override constructor of your class if you don't want the same behavior. And notice that native classes could return object when you call there constructors. Thanks for reading!

ich commented Feb 23, 2012

I think it's not clear.

In topic message with return and externalCtor it's normal, cause you know what you do and expect to get other object, not instance of the class.

But when class extends other class:

  class MyObject extends Object # or RegExp, or Function, or Array
    method: ->

  new MyObject().method()

runtime error appears: TypeError: (new MyObject).method is not a function, cause new MyObject().method is undefined, cause constructor returns MyObject.__super__.constructor.apply(this, arguments); and its just an object with no methods, cause Object.prototype.constructor.apply({}) (MyObject.__super__ is Object.prototype) returns object.

It's really unexpected! What if I'd like to make extended Array class? I should call super (оr not):

class MyObject extends Array
  constructor: () ->
    super
    @push arguments...

  trace: -> this

console.log new MyObject(4,5).method() # [4, 5]

What's a point? I think it's gonna be better to describe this case in documentation: when your class extends other and constructor of other class returns custom object, you should manually override constructor of your class if you don't want the same behavior. And notice that native classes could return object when you call there constructors. Thanks for reading!

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