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

Issue #2359 fix: avoid "other typed" constructors #2599

Merged
merged 2 commits into from Feb 1, 2013

Conversation

epidemian
Copy link
Contributor

Here's an attempt to fix #2359.

I opened the original issue quite long ago, but procrastinated ad-infinitum after feeling intimidated with nodes.coffee. #2596 encouraged me to give it another shot :)

This pull request basically re-introduces the ability to extend native objects without defining a constructor:

class MyError extends Error
console.log new MyError instanceof MyError # -> true

class Foo extends Object
console.log new Foo instanceof Foo # -> true

And also forbids returning any value from the constructor. This throws a SyntaxError:

class Foo
  constructor: -> 
    return 5

Empty returns are allowed though:

class Foo
  constructor: ->
    if mustReturnEarly
      return # OK
    @doSomeWork()

This is, however, at the expense of losing the ability to define "other typed" constructors. That is, constructors that return objects other than this. So it's a backward-incompatible change (one test had to be removed).

@epidemian
Copy link
Contributor Author

About the code of the pull-request...

The first commit is pretty straightforward, nothing fancy. But the second one is quite ugly IMO. It turned out that a couple of classes on nodes.coffee, Value and Op, were doing heavy use of "other typed" constructors. In order for the compiler to compile itself, i changed those classes so instead of instantiating them with new Value, a class method is used: Value.create. This has many problems:

  • The way of instantiating Value and Op nodes is now different than all the other nodes, which is very inconsistent.
  • There's no way of enforcing or clearly hinting that the class method create should be used instead of new (i thought of a way of forcing the constructor to be kind of private, but adding that would probably cause more harm than good).
  • In order for the Jison parser to access the Value.create and Op.create methods correctly a new regex hack was added to the little Function -> String transformer function, o, in grammar.coffee.

I didn't change the Value and Op classes to work correctly with new because i don't understand the logic behind those instantiations. Maybe using other typed constructors is the best approach for that task and therefore they should stay; i don't know.

If someone could give me a hint on how to keep the instantiations as simple new's while keeping the constructors correctly-typed, i'll gladly get rid of the create methods :). On the other hand, if the create methods don't seem like such a horrible idea, maybe we can change the other node types to use them so their usage is more consistent.

@goto-bus-stop
Copy link

Value::constructor could be altered to merge base into itself if not props and base instanceof Value and then return, and else just proceed as usual
...I think?

@jashkenas jashkenas merged commit 52b0f76 into jashkenas:master Feb 1, 2013
@jashkenas
Copy link
Owner

Beautiful work, @epidemian.

I agree that the create/wrap/new distinction (I renamed Value.create to Value.wrap) is ugly ... but I think that's the price we're willing to pay by making this decision. Those two constructors were really factory functions, not constructors, after all, and Value.wrap(literal) makes it a bit clearer that you're not necessarily always getting back a new Value.

@epidemian
Copy link
Contributor Author

Hey @jashkenas, thanks for merging this! 😸

I seems something went wrong in the merge though 😿. Value.create was renamed to Value.wrap in nodes.coffee but grammar.coffee still uses Value.create. cake build:full doesn't break on master, because the parser code in parser.js seems to be using Value.wrap. If the parser is re-generated with cake build:parser though, it starts breaking everything, as the generated parser then uses Value.create, which does not exist any more.

Maybe i'm missing something about how grammar changes are handled, but this seems like an oversight. I can fix the grammar.coffee file and push those changes into this branch if you want.


Another thing. This change introduces a backward incompatibility. Is it safe to merge it to master as is? Because this could be breaking code that relies on this kind of magic.

I think that avoiding the kind of errors reported on #2359 by not doing an implicit return super on auto-generated constructors is the right thing; it's a sane default. But at the same time, some people might have valid use-cases for other typed constructors. I don't know, it sound pretty uncool to force them to use JS-style classes. But i personally don't see any elegant option (adding special syntax for other typed constructors seems like a total overkill).

@jashkenas
Copy link
Owner

Thanks for giving it a look. I've just pushed a commit that should fix the bug with the name swaperoo.

Another thing. This change introduces a backward incompatibility.

Yep, it certainly does.

some people might have valid use-cases for other typed constructors

Not really. We've talked about it a ton -- both here, in Backbone, and elsewhere. And in pretty much every case where you want an "other-typed constructor", a regular function will do just as well, and is far clearer to read.

@epidemian
Copy link
Contributor Author

@jashkenas, the code introduced in this PR added some entropy to the code base i feel like (i.e. introducing new ways to create AST nodes for some particular kinds of them... and more regex noise in the Jison grammar DSL). Do you think there's a way cleaning that up a little bit so the creation of AST nodes gets more consistent? And do you think it would make sense to make that change?

The fix @renekooi suggested does work for the Value class; but in the case of Op i think a factory function makes more sense.

I was thinking of things like adding a create static method to each node type (most of them would call new <node type> and forward the parameters, but some of them might act differently, like Value and Op), or have some factory methods under a common namespace, like Node.makeAssign ..., Node.makeOp ..., etc. Both options are the same in the sense they are both static methods. It'd provide a consistent way of creating AST nodes for the parser, but a the same time it would add a level of indirection; i don't know it it's worth it.

@davidchambers
Copy link
Contributor

This pull request […] forbids returning any value from the constructor.

As a result, the following is no longer valid:

class Point
  constructor: (x, y) ->
    return new Point x, y unless this instanceof Point
    @x = x
    @y = y

Is the above one of the targets of the restriction, or an unfortunate victim?

@vendethiel
Copy link
Collaborator

This actually allows you to ask for bound constructord with "pretty please" :p.

@epidemian
Copy link
Contributor Author

@davidchambers IMO, yes, those "power constructors" (do they have googleable name?) would be a target of the restriction. They go hand in hand with one of JavasScript's most magical and nasty "feature": that you can call constructor functions without new and then a global object (or undefined) will be their this. I personally see no point in doing that trick: more magic in constructor code, and inconsistency (or potential for it) throughout the code base in the way that things get instantiated.

That being said, it's just my opinion. I'm interested in hearing arguments in favour of them (or a link to an article that explains their benefits).

Ping to @jashkenas: any opinion on this matter?

This actually allows you to ask for bound constructord with "pretty please" :p.

Sorry, i didn't get the joke, care to explain? 😅

@davidchambers
Copy link
Contributor

I'm currently working on a project which makes great use of CoffeeScript's lightweight syntax, as can be seen below:

'LATIN CAPITAL LETTER R':
  paths: [
    move(0,0),  (r 5)(d 1)(r 1)(d 3)(l 1)(d 1)(r 1)(d 3)(l 2)(u 3)
                (l 2)(d 3)(l 2)(u 8)
    move(2,1),  (r 2)(d 3)(l 2)(u 3)
  ]

move is actually a class, but in this context it makes sense to think of it as a verb rather than a noun, so I support calling its constructor without new.

Here are the benefits of optional new, as I see them:

  • useful for DSLs:
    • a call to a constructor can read as an instruction rather than an instantiation
    • terseness
  • same syntax can be used for both functions and constructors, which means a function can potentially be promoted to a class (or a class demoted to a function) without affecting the way in which it is used

I'm not sure where I stand on the issue, as I can see advantages to requiring new and to having it remain optional.


Having reread my comment, I've realized there's a simple way to get all the benefits of optional new in a world where new is required:

class Move
  constructor: (@x, @y) ->

move = (x, y) -> new Move x, y

Having had this realization, I'm now in favour of requiring new. :)

@vendethiel
Copy link
Collaborator

@epidemian it's been turned down several time, but now there's no way to do it in coffee.

@epidemian
Copy link
Contributor Author

@Nami-Doc Found the previous issues. Lets continue the discussion about new-less constructors there, shall we?

@davidchambers Awesome 😸. I was going to propose exactly the same code for a move function. The simplicity of just using a function instead of that magic in the constructor is unbeatable.

I'm still interested in proposals for how to clean the code changes introduced in this PR 😅

@shesek
Copy link

shesek commented Feb 26, 2013

just a quick note: @davidchambers move function can also be alternatively written with newLess [1] as move = newLess Move (and that way, you don't have to worry about updating it when the arguments changes).

[1] #861 (comment) or even simply newLess = (ctor) -> (a...) -> new ctor a... which works fine in this context, because move() doesn't need to expose the rest of the constructor API (like its methods or prototype).

edit: it should be noted that using new with splats isn't supported in a sane way in JavaScript, and requires some extra code that probably doesn't perform as well as regular new calls.

@jashkenas
Copy link
Owner

@epidemian Despite your hard work on this, I think it might be better for us to back it out before 1.5.1. Would you mind figuring out how to gracefully do the revert? Let me know if that's any bother.

@epidemian
Copy link
Contributor Author

@jashkenas, OK. No, it shouldn't bother really. It's stupid to feel attached to a change that many of us, at the time, agreed to to. It wasn't just a whim of mine.

So, back to square one i guess, right? This pull request consists of two commits: one that removes the return statement for auto-generated constructor (the thing i originally tried to fix) and another one that forbids valued returns in constructors altogether. Reverting only the second one would seem like the reasonable thing if we only want to fix #2728; but unfortunately we'll have to revert both commits to make QuickUI work again, as the desired behaviour in that case is for auto-generated constructors to return what the parent constructor returns.

That'll leave #2359 as a wont fix i suppose.

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

Successfully merging this pull request may close these issues.

Fix default constructor for subclasses of native objects
6 participants