jashkenas / coffeescript

Unfancy JavaScript
https://coffeescript.org/
MIT License
16.5k stars 1.99k forks source link

`do ->` as `(->)()` #788

Closed satyr closed 13 years ago

satyr commented 14 years ago

We all know the usefulness of this idiom, but the ugliness of the parens is intolerable. new -> works sometimes, but is flawed.

Would be really nice if we could find a way to pass arguments as well.

for l in elements then do (lmn = l) ->
  setTimeout -> lmn.click()
TrevorBurnham commented 14 years ago

Isn't the meaning of the above example the same as

for l in elements
  setTimeout -> l.click()

? I do like the idea, though. This would kill a lot of parentheses, and even cut my line count. +1.

satyr commented 14 years ago
for l in elements
  setTimeout -> l.click()

We no longer have the magical scoping within for.

hen-x commented 14 years ago

. +1. Since do is a reserved word anyway, let's make it earn its keep -- and with a nice, logical meaning!

On that note, should do object.method be meaningful? is do a full prefix equivalent to invocation parens, or does it only work with a literally-supplied block?

satyr commented 14 years ago

Implementing it as a unary operator that simply represents ()() was the initial thought, but maybe we can make it an aliase of () that special cases -> (()()) and => (().call(this)).

x * (y + z)x * do y + z

fn (if foo
  bar
else
  baz
)

fn do if foo
  bar
else
  baz

Kinda similar to Haskell's $.

TrevorBurnham commented 14 years ago

Yes, do object.method as a synonym for object.method() would be an unnecessary violation of TOOWTDI. The compiler should throw a syntax error when do is followed by anything but -> or =>, where it's clearly going to be the preferred syntax.

zmthy commented 14 years ago

This should just rescope existing variables, and automatically ensure this is preserved. We don't really need the function glyph there. do l then setTimeout (-> l.click()), 1000 ... (function(l) { setTimeout(function() { l.click(); }, 1000); })(l);

zmthy commented 14 years ago

Looking at that, it's no longer entirely clear what's going on if we just use the do keyword. The function glyph is probably a good idea simply because it ensures that the new scope is clear. do (l) -> setTimeout (-> l.click()), 1000

satyr commented 14 years ago

should just rescope existing variables

Probably. Then it becomes identical to the let proposal (#728), only with a different keyword.

TrevorBurnham commented 14 years ago

I think it's best for new scopes to always be indicated by -> in CoffeeScript.

zmthy commented 14 years ago

I've implemented this with the test: one = 1 func = null do (one) -> func = -> one one = 2 ok func() is 1 Should we ensure that this is preserved where necessary and disallow do =>? Or, seeing as how we're using the glyph anyway, should it be manual?

satyr commented 14 years ago

Maybe:

hen-x commented 14 years ago

Hard to imagine when (->)() would be preferable over (->).call(this), though. Seems like a source of errors with little gain.

zmthy commented 14 years ago

True, but I'm concerned about the ambiguity. In all other cases, a use of -> changes the context.

Perhaps a different glyph could be used here, instead of do? That way it's still clearly some kind of function so generates a new scope, but now is also clearly special and not expected to hold the same inherent properties of -> or =>. (l) +> setTimeout (-> l.click()), 1000

hen-x commented 14 years ago

Normally, the -> glyph doesn't specify a context at all; that happens at invocation. To me, the preceding do statement is introducing the context, so it's not inconsistent to let the function be defined by ->. It's just like fn = (->); fn.call(this). We're not retaining a reference to the invoked function, so it's not being permanently bound to the enclosing context, just executed there.

zmthy commented 14 years ago

Fair enough. So we just prevent use of =>, then?

What about preserving arguments?

hen-x commented 14 years ago

I say we don't forward the arguments, since it's clear that do -> represents a new function scope, and it's not possible to fake out the special properties like arguments.callee anyway.

zmthy commented 14 years ago

What do you mean by 'fake out'? If we directly pass the arguments object all of the associated properties will be there.

zmthy commented 14 years ago

Do you mean that arguments.callee should refer to the new scope?

hen-x commented 14 years ago

Sorry for being unclear. What I meant by that, was that if we defined do -> x() to compile as (function(){ x() }).apply(this, arguments), then the version of arguments within the do statement would have a different callee property value than the one outside of it. As per your fix in #792 however, this is no longer an issue :)

TrevorBurnham commented 14 years ago

I believe that do -> should mean (->)() and do => should mean (=>)() (which can be implemented as (->).call(this)). It's pleasingly consistent and makes the meaning of do nicely transparent. If, as sethaurus suggested, .call(this) is almost always preferable to () after a closure, then do => will become the standard idiom.

zmthy commented 14 years ago

Done and done. Nothing fancy, and shadows existing variables as expected. For example: do (a, b) => becomes (function(a, b) {}).call(this, a, b);

jashkenas commented 14 years ago

We can leave this on master for the time being, but I'd like to see an explanation of how/why you'd use it added to the documentation.

Right now, looking at:

do (x) ->
  x + y

... doesn't make much immediate sense. Where does the x parameter come from? We know that it has to be a variable defined in local scope ... but it breaks the consistency of all other functions, where a parameter is always passed in. Having an explanatory example would help.

StanAngeloff commented 14 years ago

Can we rename it to something like local or capture? do doesn't really hint at what's going on. Jeremy also makes a good point about breaking consistency.

TrevorBurnham commented 14 years ago

I think do is the perfect keyword, conveying the meaning "run this next function immediately." (It's a nice added bonus that it's already a reserved keyword.)

As to Jeremy's example, I do find it a little surprising that

do (x) ->
  x + y

compiles to

(function(x) {
  return x + y;
})();

I would think it would be more useful for do (x) -> to be shorthand for do (x = x) -> (which doesn't work yet)... and I see that do (x) => does work this way, so I'm guessing that's the intention and it's just half-implemented right now. Right?

Here's a common use case: It's considered a best practice in jQuery plugins to look for the global named jQuery and call it $ within the scope of the plugin (since there may be some other library that's using the global name $). So, this looks like

(($) ->
  # plugin code
)(jQuery)

Once the do syntax is fully implemented, this can be streamlined to the much clearer

do ($ = jQuery) ->
  # plugin code
satyr commented 14 years ago

I've removed the magic from do (x) -> case, so no magic on there. In do (x) =>, we can think that magic is caused by => which is already quite magical.

stephank commented 14 years ago

I haven't seen this mentioned, but how about using object literal syntax to map formal to active parameters? For example, a jQuery plugin might do:

do ($: jQuery) ->
  $.fn.wiggle = ->
    # etc.
TrevorBurnham commented 14 years ago

The do (x = y) argument-passing syntax struck me as a little off at first, too, but the object-literal syntax would be pretty incongruous. For one thing, it would be odd that

do ($: jQuery) ->

couldn't be replaced with

obj = {$: jQuery}
do (obj) ->

Here's one way to think about the do ($ = jQuery) syntax: It's functionally equivalent to

do ($) ->
  $ = jQuery
  # do things with $...

with the exception that $ is also added to arguments. So in most cases, it's just a slightly more succinct and organized way of doing initial assignments.

satyr commented 14 years ago
do () ->
  $ = jQuery
do ($) ->
  $ = jQuery
TrevorBurnham commented 14 years ago

Aren't the two functionally equivalent, satyr? Either way, arguments.length is 0 and $ is defined only within the function scope, right?

satyr commented 14 years ago

They behave differently in the presence of $ in upper scopes.

$ coffee -bpe '$ = 1; do -> $ = jQuery; blah'
var $;
$ = 1;
(function() {
  $ = jQuery;
  return blah;
})();

$ coffee -bpe '$ = 1; do ($) -> $ = jQuery; blah'
var $;
$ = 1;
(function($) {
  $ = jQuery;
  return blah;
})();
TrevorBurnham commented 14 years ago

Ah, yes, quite right. Corrected, thanks.

zmthy commented 14 years ago

Wait, what happened to the shadowing with ->? I thought that was the main advantage of this feature.

satyr commented 14 years ago

what happened to the shadowing with ->?

It's there. Exactly what I initially proposed.

for l in elements then do (lmn) ->
  lmn = l
  setTimeout -> lmn.click()
zmthy commented 14 years ago

That's not shadowing. There's not even any reason to pass in an argument unless you have another variable of the same name floating about. The reason I saw this as useful was that you could rescope existing variables.

satyr commented 14 years ago

The reason I saw this as useful was that you could rescope existing variables.

do => for ya.

jashkenas commented 14 years ago

Let's revert this, push out a point release, and we can keep talking about it.

satyr commented 14 years ago

Let's revert this

Why? It's purely additive--doesn't hurt any user upgrading from older versions.

zmthy commented 14 years ago

I don't understand why the two should act differently.

satyr commented 14 years ago

do => can't introduce a new variable like do -> can.

zmthy commented 14 years ago

Why would you want to introduce a new variable? You've already created a new scope, you just need to assign to to it within that scope. It results in the same thing.

jashkenas commented 14 years ago

Why? It's purely additive--doesn't hurt anyone.

Adding unsettled features that are only half-baked, are still controversial, and are likely to still undergo change and/or be removed, does hurt people. If we push it out, folks start to depend on an unstable feature, and in the meantime it delays point releases. This is the sort of thing that belongs on a branch as it's being discussed. Reverted here:

http://github.com/jashkenas/coffee-script/commit/5b16d4790c54b63437039da890325e697ff25a49

satyr commented 14 years ago

Why would you want to introduce a new variable

For tighter variable scopes which can't be achived with CS's assignment system. Maybe read the issue title/body again?

satyr commented 14 years ago

In an IRC discussion on the reverted implementation, we didn't like the fact do func doesn't work (inconsitency) and the magical scoping (confusion).

So the proposal is back at original; simple and primitive call:

, mirroring new nicely. Opinions?

zmthy commented 14 years ago

I still think that automatically passing variables in when a function is defined with the do keyword is a good idea. For example: for value, index in array then do (value, index) -> ... is significantly cleaner than: for value, index in array then do -> val = value i = index ... You also end up with less variables over the place. It's a quick and tidy explicit solution to the now removed for magic.

I don't see any reason to pass a function declaration with variables to the do keyword unless this was the case. I don't agree with satyr's argument that it would enclose possibly nonexistent variables, just in case. The whole point of why we don't currently support shadowing is because you should be coding well enough to not need it for 'just in case' scenarios, and having do should allow you to group your variables into scopes anyway, preventing any such problem from arising.

The whole point of bringing the discussion back out here was to get the opinion of everyone involved, though. Let's hear who prefers what, then make a decision.

satyr commented 14 years ago
 for value, index in array then do (value, index) ->

That's kind of a different issue--this issue is about removing parens. And even with the magic, it still requires you to write variable names twice--not optimal. You'd rather have:

for var value, index in array ...

or something with the same semantics.

hen-x commented 14 years ago

What if we defined do to behave like this? do = (args..., block) -> block args... Then, it would be possible to pass arguments to the block like this: for value, index in array then do value, index, (v, i) -> It's verbose, but at least it doesn't overload the meaning of (value, index) ->.

satyr commented 13 years ago
do = (args..., block) -> block args...

I'd worry that it'd be needlessly comlicated:

do a(), b(), (c())(_ref = a(), _ref2 = b(), (c())(_ref, _ref2))

with only small improvement from:

for value, index in array then do (v, i) -> v = value; i = index
StanAngeloff commented 13 years ago

I like sethaurus approach because you can already do it in Coffee without adding new stuff to the core.

local = (args..., block) -> block args...
for i in [1..2] then local i, (i) -> console.log i

-1

satyr commented 13 years ago

you can already do it in Coffee without adding new stuff to the core

Right. Core stuff should be simple.

jashkenas commented 13 years ago

So is the consensus that you can do it with a helper function instead of the do keyword?