jashkenas / coffeescript

Unfancy JavaScript
https://coffeescript.org/
MIT License
16.52k stars 1.98k forks source link

Syntax for indicating that a function should not return a value #899

Closed TrevorBurnham closed 13 years ago

TrevorBurnham commented 14 years ago

This is a fork from issue 896. To review: It's common to write a function that performs a loop at the end but where the list generated by that loop should not be returned. The best current solution is to add return to the end of the function, but this feels a bit kludgy, especially in one-liners. For instance:

showInts = (arr) -> console.log x for x in arr when isInt(x); return

Failing to do this is a common pitfall not just for CoffeeScript newcomers, but even for pros. Look at underscore.coffee, where _.mixin and _.times both generate lists, making them far less efficient than their underscore.js brethren. I think this speaks to how unintuitive the trailing return is.

So, I propose that putting void in front of -> or => should tell the compiler: "This function is not supposed to return a value. Don't generate returns implicitly. And raise an error if return anything explicitly." Then the example above would become

showInts = (arr) void -> console.log x for x in arr when isInt(x)

And underscore.coffee wouldn't have to grow 2 lines longer in order to fix _.mixin and _.times:

_.mixin = (obj) void ->
  for name in _.functions(obj)
    addToWrapper name, _[name] = obj[name]

The void -> syntax is not only more succinct, but also more self-documenting than the return syntax. If I'm relatively new to CoffeeScript and I see a trailing return, I'm baffled—what is that doing there? Whereas if I see void, a keyword that isn't allowed in any other context in CoffeeScript, I can look it up immediately in the documentation.

Thoughts?

[On further reflection: Instead of void, how about *->? For instance, compare

readFromDatabase (result) *-> console.log result

to the current

readFromDatabase (result) -> console.log result; return

A pretty significant readability improvement, don't you think?]

jashkenas commented 13 years ago

Nope, Trevor, sorry ... We don't need to introduce a new keyword to accomplish something you can already be doing by adding a null to the end of the function. I apologize for _.mixin and _.times, my only excuse is that I was doing a direct JavaScript port.

CoffeeScript forces you to adopt the habit that you have to think about having a good return value for every function you write -- and I think it's a great habit to have. There's a lot of JS out there that would benefit from better return values, even for side-effect-ful code. Making it easier to create value-less functions is the opposite direction.

delfick commented 13 years ago

Can we at least use such a syntax to specify a default return value in the signature? (i.e. true, 0, 1, null, undefined, {}, [1,2], etc)

So that if return isn't used before the function is ended, then that default value is used.

This would be very helpful for functions that creep to the right quite a bit due to many nested functions, where you still want to return some value at the bottom. Currently, you end up created a triangle

someFunction () ->
    anotherFunction ->
        # do some things
        # and some more things
        # etc

        anotherGetter ->
            # and some more logic
            # logic is good for the brain
            # stuff

            moreNesting ->
                # Sometimes it's necessary
                # for this amount of nesting
                # so far we've created a diagonal line
                # Yay for no closing braces !

    # and we still need to return true
    # and we've created a triangle
    true

Possibly even take some inspiration from google go and be able to define in the signature the variable in the body of the function that should be returned......

:)

TrevorBurnham commented 13 years ago

I keep thinking back on this issue as I work, and I'd like to see it reopened.

I agree with the philosophy behind implicit return values, and I certainly want to keep them. But the current situation forces CoffeeScript coders unnecessarily to choose between writing clean-looking code and truly efficient code. This isn't an issue in Ruby, partly because bytecode size isn't an issue in Ruby, but mainly because Ruby doesn't have implicit list comprehensions.

For instance, the following code looks elegant, but is in fact extremely inefficient—in terms of byte code, performance, and most of all memory—since it creates and returns a deep copy of canvas:

applyPixelFilter = (canvas, pixelFilter) ->
  for x in [0...canvasWidth]
    for y in [0...canvasHeight]
      canvas[x][y] = pixelFilter canvas[x][y]

Granted, adding a return here isn't too big a deal. But as delfick says, the aesthetic problem gets worse as you nest functions; and especially in asynchronous code, it's very desirable for functions to not return anything.

I don't think this has to do with whether programmers have to think about what functions return or not. Whether there's a special syntax for it or not, we're returning the last expression value from a function unless the programmer specifies otherwise.

I think delfick's idea of being able to specify a default return value for a function (that is, something that's returned unless the return keyword is used) is an excellent generalization of my proposal. Perhaps the syntax I suggested isn't ideal, but something that lets a return value (limited to, say, true, false and void) be declared at the top rather than at the bottom would be a huge aesthetic win. Using the reserved default keyword to call attention to it might work.

TrevorBurnham commented 13 years ago

Here's another proposal: Let *-> and *=> indicate functions that don't return values. Compare

_.times = (n, iterator, context) ->
  iterator.call context, i for i in [0...n]
  return

to

_.times = (n, iterator, context) *->
  iterator.call context, i for i in [0...n]

I find the latter both more writable and more readable. I keep hearing complaints about the need to give explicit returns, even in anonymous functions, to prevent excess code from being generated. This syntax would, in my view, be a major improvement, consistent with the core philosophy of the language.

TrevorBurnham commented 13 years ago

There's been some recent discussion about this on the Google Group.

As I've continued to work in CoffeeScript, I've developed a habit of adding return at the end of nearly every callback to prevent unwanted side effects. Not every idea I've had for improving this situation still seems wise to me, but having *-> and *=> to declare functions as "side-effects only" does. I hope it'll be reconsidered.

sxross commented 13 years ago

Continuing with Trevor's comment above, I believe that the additional clarity lent the code written with a -> or => operator is desirable. As a Rubyist, I expect methods to return the last evaluated value. That is not always common Javascript expectation and Coffeescript is, after all, "only Javascript", hence should enable common usage patterns when possible.

As a further note, clarity has the benefit of making the code easier to read and less error-prone during a refactor. One cost associated with a language that is whitespace-sensitive is that lining up scopes can be, at times, daunting. Sprinking in explicit returns just for the sake of returning nothing can introduce errors and/or make refactoring difficult.

satyr commented 13 years ago

The look of *-> is puzzling. void seems clearer to me.

sxross commented 13 years ago

The syntactic sugar, IMO is secondary to the actual decision to add an operator. However, if it does get down to syntax, I agree that the asterisk connotes less than the word void about the expected return value.

TrevorBurnham commented 13 years ago

Sure, the initially proposed void -> syntax would also be acceptable. That would, at least, give the syntax a name. If you tried to write something like

sideEffectsOnly = void ->
  console.log 'This function is only supposed to produce side effects'
  return false

then the compiler could give you the error void functions cannot return a value.

caseyohara commented 13 years ago

I agree with satyr. While void is certainly more keystrokes than *, it is less than return. It's more succinct than both.

satyr commented 13 years ago

console.log 'This function has no side effects'

Self-contradiction? (Logging is a side-effect.)

TrevorBurnham commented 13 years ago

Oops, typo. Corrected, thanks.

Not sure if there's a good term for such functions. "Anti-pure functions"? "Void functions" works well for our purposes.

michaelficarra commented 13 years ago

@TrevorBurnham: Why would void have to apply only to functions? We could just introduce the JS void operator which would allow us to write expressions like void inner_expr that produce an undefined value. Also, a lower precedence than in JS would be nice to allow void (1 + 2) to be unparenthesized.

TrevorBurnham commented 13 years ago

@michaelficarra Hmm. But then void would have two very different meanings in CoffeeScript: one for functions, one for everything else. Is void inner_expr really a useful idiom in CoffeeScript?

michaelficarra commented 13 years ago

@TrevorBurnham: The meanings would be the same. It is the same operator in both places. Can you outline any differences?

$('something').click -> void do =>
  'do something'

and with arguments:

$('something').click (x, ...) -> void do (x, ...) =>
  'do something'

I still don't even think this is an issue, but at least the void operator is a better solution than another funky arrow glyph (or two!). Appending a return to explicitly specify a return value (even an undefined one) makes sense. When you don't care about the return value, just forget about it.

TrevorBurnham commented 13 years ago

@michaelficarra

$('something').click (x, ...) -> void do (x, ...) ->
  'do something'

is very different from the proposed

$('something').click (x, ...) void ->
  'do something'

In addition to DRY concerns, the first approach discards this (unless => is used on the inner function), and adds the overhead of an extra function call each time the callback is invoked. Would you really use that technique?

Clarification: The proposed compilation of the given example is

$('something').click(function(x, ...) {
  'do something';
});

void just removes the return.

As to "When you don't care about the return value, just forget about it.": I started adding return to callbacks as a matter of habit not because I'm nitpicky or efficiency-obsessed, but because it was becoming a major source of bugs. I wasted a lot of time before I realized that Vows interprets any return value from a callback other than undefined as meaningful. I got tired of having to look through documentation or source code of every new library I tried to check whether the return value on callbacks was significant. And as folks on the Google Group have pointed out, the abundance of accidental returns hurts the readability of JS output.

michaelficarra commented 13 years ago

@TrevorBurnham:

Whoops, forgot =>. Fixed. And no, I wouldn't use that technique because (as I described in the final paragraph) if I want to specify a return value other than that of the last expression, I will use an explicit return statement. In my opinion, that should be the one and only way to specify a return value. I believe it was you who taught me the python philosophy "TOOWTDI". I was just putting forward this void operator idea as a slightly more versatile way to give you a syntactic construct that will cause a function to produce undefined.

edit in response to your edit: The compilation can be simplified down when that pattern is recognized, but that's besides the point. Your argument regarding libraries whose behaviour is dependent upon the return value of your function is not very convincing. Either you care about the return value or you do not. And some of those times when you do, that return value is not the value of the last expression of the function body. In libraries like the one you mentioned, you do care about the return value and you do want it to to be different than the last expression. That's the only time the return keyword is used at the end of a function body. And you're suggesting we specify at the function head (through use of an alternative arrow syntax) that we would like to emulate the effects of a return without any expression being tacked onto the body? That just seems nowhere near necessary to me. And definitely not TOOWTDI. More like Perl's "There's An Unreasonably Large Number Of Ways To Do It" (tm).

TrevorBurnham commented 13 years ago

@MichaelFicarra I suppose that adding void as you propose it would at least solve the one-liner problem elegantly. To rewrite my original example, instead of writing

showInts = (arr) -> console.log x for x in arr when isInt(x); return

you could write

showInts = (arr) -> void console.log x for x in arr when isInt(x)

and the compiler, seeing a void expression on the last line, would compile the function with no list comprehension and no return. It'd avoid the visual awkwardness of the glyph, and the potential issue of having to look at the start of a function to see whether it returns a value or not.

Note that I'm assuming precedence ordering such that the above would be equivalent to

showInts = (arr) -> void (console.log x for x in arr when isInt(x))

rather than the less helpful

showInts = (arr) -> (void console.log x) for x in arr when isInt(x)

So, I'm warming to your suggestion. It feels like a good compromise. Essentially, void would function as the anti-return: "Don't return this expression." I'll happily take

void expression

over

expression; undefined
aseemk commented 13 years ago

After reading this whole thread, I really have to +1 Trevor's points and the proposal for the void operator.

Personally though, I'll chime in that I'm not a huge fan of automatic/implicit return values for multi-line functions. I get the beauty of not having to write "return foo" for one-liner functions -- just like Python's lambdas -- but in multi-line functions, it's just not obvious when you're reading a function and you don't see the word "return" that a value is getting returned. Because of that, I've tended to generally use the keyword "return" for returning values in non-trivial functions even though I don't need to. And yes, I've had to explicitly return nothing at the end of functions when I mean a function to be void.

masonmark commented 13 years ago

I'd also like to convey my +1 for Trevor's proposal ('void' or '*' or whatever the final syntax is). The main reason is that this makes the code more self-documenting.

I really appreciate the implicit return feature of CS. But there will continue to be many cases where return values are explicitly unwanted. In those cases, having the first line of the function definition unambiguously tell you 'this function does not return a value' is very useful. (Even more so when you use an editor that can collapse functions--otherwise you have to uncollapse the function and inspect its code to see whether it has a meaningful return value.)

coreh commented 13 years ago

Just tripped over a performance issue caused by the implicit return of a list comprehension today, so +1 for this proposal.

I would only like to suggest the following shorthand for -> void, instead of the already proposed *->:

-/>

I think it looks pretty cool, and it kind of gives the impression that the flow of information is somehow "cut" or "forbidden"

f = (list) -/>
  for item in list
    console.log item

It's only a shorthand though, so it would work the same as using -> void.

Bound functions could then use the similar =/> shorthand.

TrevorBurnham commented 13 years ago

Wow. I like that syntax a lot.

On Jun 26, 2011, at 11:36 AM, Corehreply@reply.github.com wrote:

Just tripped over a performance issue caused by the implicit return of a list comprehension today, so +1 for this proposal.

I would only like to suggest the following shorthand for -> void, instead of the already proposed *->:

-/>

I think it looks pretty cool, and it kind of gives the impression that the flow of information is somehow "cut" or "forbidden"

f = (list) -/> for item in list console.log item

Bound functions could then use the similar =/> shorthand.

Reply to this email directly or view it on GitHub: https://github.com/jashkenas/coffee-script/issues/899#issuecomment-1441593

trans commented 13 years ago

Do I understand correctly? The proposal is for this:

f = (list) -/>
  for item in list
    console.log item

instead of already valid:

f = (list) ->
  for item in list
    console.log item
  null
trans commented 13 years ago

Note in Ruby returning method was invented, but it doesn't appear to get lots of use.

P.S. Let me add a pseudo example:

f = (list) ->
  returning undefined ->
    for item in list
      console.log item
aseemk commented 13 years ago

I dig the appearance of -/>, but I also dig the readability and clarity of the void keyword. =)

TrevorBurnham commented 13 years ago

@trans Well, with undefined or return rather than null, yes.

@aseemk I dig the readability and clarity of void -> as well. What I don't like is the look of either (foo, bar) void -> or void (foo, bar) ->. That's why I think a different glyph would be preferable, and Coreh's proposed -/> stands out brilliantly.

caseyohara commented 13 years ago

I also like @Coreh's propopsed -/> syntax. Agreeing with Trevor, void loses its appeal when the function has arguments.

trans commented 13 years ago

It strikes me as too much of a perlism.

michaelficarra commented 13 years ago

@trans: void, the JS operator, is a "perlism" to you? Or were you speaking about the newest arrow syntax proposal?

I still think this discussion is ridiculous. Also:

$ coffee -bep 'a = -/> b/i'
var a;
a = -/> b/i;
trans commented 13 years ago

newest arrow syntax proposal

coreh commented 13 years ago

@michaelficarra I had thought about this, but I don't think it's going to be an issue for the parser. As far as I know, lexers produce tokens on a greedy manner, so -/> should be recognized as a token just fine.

If you're talking about breaking existing code, (since this addition will change the meaning of already valid programs) I don't think that's an issue either: I don't think any serious code is going to be subtracting regular expressions, as that will always result in NaN.

Of course, the code might call some of the methods of the regexp literal, which in turn could return a number, and the code would then subtract that. But such a combination of events (minus sign, regexp literal starting with > character, calling one or more functions on the literal that somehow return a number) is so rare, that I don't think it's going to be an issue.

Anyway, regardless of the syntax, I'm interested in the void functionality.

lackac commented 13 years ago

+1 to this proposal. I've been bit by coffeescript's always returning strategy as well. Once it was with a function iterating over millions of rows while keeping all of it in memory and it's always an issue when using vows for testing. Our solution for vows was to create a global function U which would create a new function from the argument which returns undefined:

# helper for creating functions with explicit undefined return value
global.U = (f) -> -> f.apply(this, arguments); return

then we would use this helper to create asynchronous topics:

  "some context":
    topic: U -> Model.find('some_id', @callback)
    "some tests": (doc) -> # ...

I like the proposed -/> syntax, but you should also consider ~>. It's as simple as the other function operators (-> and =>) and I don't think it would be a problem to parse.

trans commented 13 years ago

@lackac how did implicit return keep everything in memory? seems strange. and why all the trouble with U when you could have just added an undefined to the end?

The problem with the perlism (-/>) is that it's not functional in any other way. More benefit would com from a way to state any return value upfront.

f = (list) ->
  return undefined ->
    for item in list
      console.log item

Or

 f = (list) -|undefined|->
    for item in list
      console.log item
tarsolya commented 13 years ago

+1

lackac commented 13 years ago

@trans the function was iterating over millions of documents each of which got added to the _results array. Many of those documents were very big...

Now, I'm not saying that this was CoffeeScript's fault. Once I realized what the problem was I was able to fix this issue with returning explicitly at the end of the function. I just think it would be nice to have something for these kind of functions in the language.

As for the vows case, when you have a lot of asynchronous topics in your suite, having to add an explicit return to all of them makes it very ugly. The U helper cleaned up our test suites quite a bit.

TrevorBurnham commented 13 years ago

@lackac The problem with ~> is that it's practically indistinguishable from -> in many fonts (including this one...). I like how -/> really stands out. It's easy to spot, and easy to remember.

@trans In the abstract, I like the idea of being able to specify a return value up-front. But consider how it would work in practice. If I were to write

f = -|this|-> ...

then would that be equivalent to making this the last line of f, or would it capture the value of this where f is defined and return that? It's syntactically very unclear.

-/>, by contrast, is beautifully clear, and addresses the main pain point: having to add return to callbacks.

trans commented 13 years ago

@TrevorBurnhma I understand what you are saying. But one thing I learned over the years, when a new "special syntax" is introduced it should always set off red flags. It may look "beautiful" for the isolated case, but when you take all such things en mass you get hard to read code. Hence the term a "perlism". It's bad for beginners especially, and it means another bit of memorization everyone must do. I would much rather see return have some sort of special "up front usage". (Note, I'm not too fond of -|foo|-> either, was just throwing it out there as brain juice).

coreh commented 13 years ago

@trans But isn't the fat arrow/bound function already some sort of special syntax? The same goes for @, ::, and the ? operator.

Actually, the ? operator is even trickier for beginners, because it has different meanings depending on the space around it:

a? 1

compiles to

if (typeof a === "function") {
  a(1);
}

while

a ?1

compiles to

if (typeof a !== "undefined" && a !== null) {
  a;
} else {
  1;
};

But that doesn't mean the ? operator is bad, quite the contrary, it's one of the best features of coffeescript.

sxross commented 13 years ago

In Rails, there used to be a construct:

returning foo do

something

end

This isn't language magic -- it's Ruby magic, so without thinking about it too much I can envision:

returning(undefined, ->

something

)

Which you could special-case to:

voidReturn(->

something

)

This is like the U function mentioned previously. Was there a problem with this "promise" DSL? Do you remember, @trans?

Sent from my iPhone

On Jun 27, 2011, at 9:19 AM, Corehreply@reply.github.com wrote:

@trans But isn't the fat arrow/bound function already some sort of special syntax? The same goes for @, ::, and the ? operator.

Reply to this email directly or view it on GitHub: https://github.com/jashkenas/coffee-script/issues/899#issuecomment-1448443

trans commented 13 years ago

@sxross

The only issue that I know of is that it adds some extra overhead, so it slows a program down a tad. Same things about #inject btw. So most experienced coders end up avoiding both.

With node, the transpiler could avoid any such overhead, so that should not be a problem. So in this case I'm thinking void and a special return syntax that look like regular code more or less but get some special treatment.

trans commented 13 years ago

@Coreh Of course there are always going to special notations. Those are good things and necessary. But it doesn't mean everything can or should be so. It's a "Goldilocks". So we should be diligent and only add such things if they are really really really better.

To get an idea of just how crazy things can get in this regard check out APL. Here is an example: http://aplwiki.com/PascalsTriangle/Solution.

AlexJWayne commented 13 years ago

+1 for -/>

satyr commented 13 years ago

-/> kind of gives the impression that the flow of information is somehow "cut" or "forbidden"

Exploring further:

caseyohara commented 13 years ago
davidchambers commented 13 years ago

There's a pleasing symmetry between -> => and .> :>. Additionally, two characters is better than three, and . does a decent job of conveying "nothingness" (though not nearly as good a job as void).

Forced to choose right now, I'd vote for .> and :>. That said, I don't have a strong opinion as to whether such syntax should actually be added to the language.

RyanScottLewis commented 13 years ago

-> => -] =]

To me, the greater than symbol indicates to "pass" and the right bracket indicates "stop".

jashkenas commented 13 years ago

I think that now that this discussion has settled down to symbol bikeshedding, it's ripe time to give it mercy.

Creating a special type of function to indicate that it should not return a value is a very poor idea. Let's go through the points:

The only places where you'd want to use void are places where you're aware that you want to return undefined -- either because you're concerned about the efficiency of that particular function, or because the API is asking for it. In either case, it's just as easy to write return as it is void.

Every function returns a value, even if that value is nothing more than undefined. If everything in CoffeeScript is an expression ... then every function should be a value, and calling any function should return a value.

If the shape of a function in CoffeeScript is (input) -> output then the natural and proper place to look for the output of a function is at the end of a function body -- early returns aside. Tagging a function body as void up front puts that declaration in the wrong spot.

The biggest problem with this proposal is that is makes it possible, easy even, to write hypocritical code:

showInts = (list) void -> 
  console.log x for x in list when isInt(x) 
  return list

Now your code says it's going to do one thing, and actually tries to do another. The syntax of a language should not make this sort of nonsense easy to write.

Closing the ticket.

TrevorBurnham commented 13 years ago

Jeremy, the proposal was (at least in my mind) to not only disable implicit returns in these "void functions," but also to make explicit returns like those in the hypocritical code you describe a syntax error. (Ending such a function with a no-op like "2+2" would, ideally, yield a warning.)

It's rare for there to be this much interest in a feature based on actual experience using CoffeeScript. I hope you'll reconsider it.

On Jun 30, 2011, at 10:14 PM, jashkenasreply@reply.github.com wrote:

I think that now that this discussion has settled down to symbol bikeshedding, it's ripe time to give it mercy.

Creating a special type of function to indicate that it should not return a value is a very poor idea. Let's go through the points:

The only places where you'd want to use void are places where you're aware that you want to return undefined -- either because you're concerned about the efficiency of that particular function, or because the API is asking for it. In either case, it's just as easy to write return as it is void.

Every function returns a value, even if that value is nothing more than undefined. If everything in CoffeeScript is an expression ... then every function should be a value, and calling any function should return a value.

If the shape of a function in CoffeeScript is (input) -> output then the natural and proper place to look for the output of a function is at the end of a function body -- early returns aside. Tagging a function body as void up front puts that declaration in the wrong spot.

The biggest problem with this proposal is that is makes it possible, easy even, to write hypocritical code:

showInts = (list) void -> console.log x for x in list when isInt(x) return list

Now your code says it's going to do one thing, and actually tries to do another. The syntax of a language should not make this sort of nonsense easy to write.

Closing the ticket.

Reply to this email directly or view it on GitHub: https://github.com/jashkenas/coffee-script/issues/899#issuecomment-1480351

trans commented 13 years ago

Wouldn't something like ruby's returning method suffice, and be more versatile and not suffer the speed penalty of Ruby's thanks to transpiler optimization. Something like:

showInts = (list)
  return undefined -> 
    console.log x for x in list when isInt(x) 

?

TrevorBurnham commented 13 years ago

The return x -> syntax is a non-starter in CoffeeScript; because functions can be passed around, return x -> already has a valid meaning. And even if that weren't an issue, I think the syntax would hurt rather than help readability.