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

Implement void operator #3731

Closed
wants to merge 3 commits into from
Closed

Implement void operator #3731

wants to merge 3 commits into from

Conversation

epidemian
Copy link
Contributor

While this PR might seem like an uncalled attempt of bringing a weird operator from JavaScript nobody seems to miss back to CoffeeScript, it's actually a little stab at trying to solve #2477, i.e., "Remove implicit returns?".

But without removing implicit returns 😺

tl;dr

void allows fixing the problems cited by @Zyphrax in an arguably more elegant way than adding returns everywhere. For example, this tricky jQuery event handler problem (source) can be fixed by "voiding" the last boolean assignment:

# Toggle when the toggle button is clicked
nextState = off
$('#btn').click ->
  $('#state').text if nextState then "On" else "Off"
  void nextState = !nextState

var nextState;

nextState = false;

$('#btn').click(function() {
  $('#state').text(nextState ? "On" : "Off");
  nextState = !nextState;
});

Or, if you want to avoid creating a big array because the last expression of your function is a for comprehension, void can save you there too!

x = (n) ->
  void for i in [0...n]
    dostuff i

var x;

x = function(n) {
  var i, _i;
  for (i = _i = 0; 0 <= n ? _i < n : _i > n; i = 0 <= n ? ++_i : --_i) {
    dostuff(i);
  }
};

This way, void retains basically the same meaning from JS: it evaluates the expression we pass it and returns undefined, with the added benefit of the compiler being able to avoid generating a return statement if void is the last expression of a block because it knows about its semantics.

But why not a different function symbol?

I actually was in favour of using !-> or -/> to denote procedures. But it has quite a nasty problem: the combinatorial complexity of having various "modifier" behaviours for functions. Now we have only one modifier, whether the function is "bound" or not, so we have two symbols: -> and =>. Adding an extra modifier, whether it returns useful values or not, would require having four symbols: ->, =>, !-> and !=>. Now image if we decided to implement generators as yet another modification of the function syntax... yes, eight(!!!) symbols: ->, =>, !->, !=>, ->*, =>*, !->* and !=>*.

It's not only about the grammar complexity, but mostly about the cognitive load of having to pick between many different function types whenever you want to write a damn function. Right now the picking is pretty easy: always pick ->, and if later on i discover that i need the this of the parent context, change it to =>. Done. And it should stay that simple 😉

So, why void then?

I don't know, it just occurred to me when considering alternative solutions for the sour #2477 issue. At first i thought it was a stupid idea, but after giving it a try it started to make some sense.

First, i think it fits Coffee's philosophy of "everything is an expression" quite nicely. void <expr> is itself just an expression, that just happens to always be undefined.

Also, i think that Coffee/Ruby programmers develop a habit of looking at the bottom of blocks to see what they evaluate to. Ideas like the !-> function syntax or specifying the return value before the function body break that expectation, while void doesn't.

The fact that void concept is independent of the "function type" is also a big win. It avoids the possible combinatorial explosion of complexity mentioned above.

And it's also worth noting that void is already a part of JavaScript; it's not a new concept. It's even a reserved keyword on today's CoffeeScript, so it's not like this change could break code that uses void as an identifier.

Sooo... should i then start using void at the end of most of my functions?

Simply put: no! I mean, you can. But it's not in the spirit of this proposal to have void as something one would use in the general case of writing procedural functions.

For example, it doesn't make sense to have all your test case functions end on void because you don't care about the return value. If you don't care, just leave the last expression be returned, and trust that the testing framework won't care about that return value either.

void should be used sparingly, to mark cases where it is important that the function returns undefined:

secretLocked = yes
$('#hidden-secret-unlocker').click -> void secretLocked = no # We don't want the click event to stop bubbling up.

Well, that's all, i hope this proposal can bring an adequate solution for people having trouble with implicit returns, while at the same time not make the language much uglier for people not having problems with them.

What do you people think? 😺

expr = @expression || new Literal '0'
[
@makeCode("void(")
expr.compileToFragments(o, LEVEL_LIST)...
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if using the LEVEL_LIST here is OK. Basically i tried with different values until void if a then b else c compiled to void(a ? b : c) instead of void if (a) { b } else { c } or void((a ? b : c)). Does this make sense?

@vendethiel
Copy link
Collaborator

Something similar was proposed for coco: satyr/coco#218.

What's wrong with a ml-style ignore = -> then

myFn = ->
  cb a for a in list
  ignore secretLocked = no

(Well, I guess it's gonna bite you if you try to use spaced-dot and stuff like that... precedence...)

@epidemian
Copy link
Contributor Author

What's wrong with a ml-style ignore = -> then

Compared with this void proposal? Well, it still generates a JS return, plus an unnecessary function call. And it doesn't address the creation of an unnecessary array when the last expression is a loop, which i think is the most valid complain about the implicit returns.

It's funny however that the first basic implementation of void i tried was basically that. I just removed "void" from the reserved keyword list, thus allowing void something to be treated as a function call in CS, and compiled to void(something) in JS.

@vendethiel
Copy link
Collaborator

And it doesn't address the creation of an unnecessary array when the last expression is a loop, which i think is the most valid complain about the implicit returns.

What do you mean?

ignore = ->

parse = (comments) ->
  for comment in comments
    send comment

  ignore() # right, need to call ignore not to be truthy
  # ^ did not generate loop values

Let's try a benchmark: http://jsperf.com/void-vs-ignore-vs-noreturn
This is with 50 iterations

@connec
Copy link
Collaborator

connec commented Nov 19, 2014

ignore (for comment in comments then send comment)

as compared to:

void for comment in comments then send comment

If you're going to end your function body with ignore() then you may as well end it with undefined.

@vendethiel
Copy link
Collaborator

If you're going to end your function body with ignore() then you may as well end it with undefined.

Yes, I mentioned the issue of precedence. The "only" advantage ignore has is that you can still put stuff "on the same line"

@connec
Copy link
Collaborator

connec commented Nov 19, 2014

I quite like this idea from the perspective of writing code, though I don't think it gives us anything that undefined doesn't already give us from the perspective of reading code (e.g., if you're already looking in the function for return values, is it better to see void <expr> or just undefined).

@michaelficarra
Copy link
Collaborator

I can't see introducing this feature just to avoid writing return on the last line for procedures.

@jashkenas
Copy link
Owner

@michaelficarra —quite right.

Adding a void operator is a neat idea to "void-out" the value of an expression, in general ... but it compares quite unfavorably for the proposed use case.

func = ->
  void (send item for item in list)

Isn't nearly as nice as:

func = -> 
  send item for item in list
  return

So — without any good reason for wanting to "void" the value of an expression in general — the value is almost always a useful thing — let's not do this.

@akre54
Copy link

akre54 commented Nov 19, 2014

What about moving the void to the function operator instead of the last statement?

nextState = off
$('#btn').click void ->
  $('#state').text if nextState then "On" else "Off"
  nextState = !nextState

@vendethiel
Copy link
Collaborator

This one proposal has the same problems as exposed in every previous issue about this.

@epidemian
Copy link
Contributor Author

Ok, i tried 😛

Adding a void operator is a neat idea to "void-out" the value of an expression, in general ... but it compares quite unfavorably for the proposed use case.

How about void having very low precedence so that you can "void-out" any last expression by prepending void to it?:

func = ->
  void send item for item in list

(This is actually the way it's implemented in this PR, as i wanted to support the second example shown in the description.)

What about moving the void to the function operator instead of the last statement?

That has basically the same problems of using a special function symbol !-> (described on the PR description).

@akre54
Copy link

akre54 commented Nov 19, 2014

That has basically the same problems of using a special function symbol !-> (described on the PR description).

Right, but it doesn't introduce a new symbol with its endless permutations (as you pointed out). The void operator is simply an instruction to the compiler.

@vendethiel
Copy link
Collaborator

Right, but it doesn't introduce a new symbol with its endless permutations (as you pointed out). The void operator is simply an instruction to the compiler.

I still see it as a permutation, though. The way it's implemented on coco, it's just the "not" operator, not something else (not -> 1 works)

@GoToLoop
Copy link

Alas... I wanted a clean void operator in order to avoid returning values from loops at the end of function!

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

Successfully merging this pull request may close these issues.

7 participants