jashkenas / coffeescript

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

Remove implicit returns? #2477

Closed devongovett closed 11 years ago

devongovett commented 11 years ago

After writing CoffeeScript for quite a long a while, I've come to the realization that implicit returns are actually more annoying than useful. Oftentimes I'll have to go back to a function later and add an explicit return statement to prevent CoffeeScript from adding implicit returns. They are the source of many bugs, and reduce the readability and predictability of CoffeeScript code.

  1. Having to go back an add explicit returns happens especially often when a loop is the last thing in the function, where very rarely do I want the function to return an array (if I did, I could easily add an explicit return). Generating and returning that array when I very rarely want it can also have a potentially large performance impact on the function as well, so I always have to remember to add a return statement at the end of many of my functions to prevent this. Just looking at a function with a loop at the end doesn't tell me that it returns an array so when it turns out it does it can be kinda strange. Usually you end up finding out when you discover another bug related to it or you look at the compiled JavaScript.
  2. It can also be inconsistent when combined with conditional logic. For example:
test = ->
        if someCondition
            doSomething()

This function returns the result of doSomething() only if someCondition is truth-y, otherwise it returns undefined. The question is why I would ever want a return value sometimes and not others. Without an explicit return statement, this seems strange. It isn't clear that this function returns anything just from looking at it.

  1. It can be annoying for functions that do something based on another function's return value. An example of such a function is jQuery's each function, which uses the false sentinel return value as a way to break out of the loop. Using the function in CoffeeScript when the last operation in that function sets a variable to false can be a source of bugs since CoffeeScript implicitly returns false and this stops the loop. For example:
arr = ['a', 'b', 'c']
map = {}
$.each arr, ->
        map[this] = false

Just reading this example doesn't make it clear that the loop will only run once rather than the expected three times. The variable map would equal { a: false } after running this loop. I would have expected to have gotten { a: false, b: false, c: false }. Adding an explicit return as the last line of the each function fixes the problem, but you wouldn't know to do that without reading the compiled JavaScript.

I could probably come up with many more examples to show that implicit returns are usually not a good idea. I think CoffeeScript should actually remove implicit returns from the language. Typing out return really isn't so hard and will lead to more readable and predictable code and keep the language closer to the semantics of JavaScript (a good thing when working with libraries written in JavaScript like jQuery from CoffeeScript code). It would probably be less typing in the end since you will no longer have to add return statements to prevent CoffeeScript from implicitly returning the wrong thing. I know this would be a large change but I think it would be for the better since it is more of a cause of bugs than a helpful language feature.

Removing features from a language is hard, but sometimes the best thing to do. What do you think?

matthewrobb commented 11 years ago

I'm not sure I fully agree but last week I actually ran into a situation where I had to add an explicit return. I'm writing a tool in node that uses Dust templating and allows a person to utilize their own scripts in order to add helpers to the context before compiling.

The source for this tool is written in CoffeeScript but up to this point the helper scripts were being authored in JavaScript. I recently made it possible for them to be authored with either and when writing tests/examples it doesn't help to show the benefit of CoffeeScript when I had to add all these seemingly meaningless returns in to the helper functions due to the fact that when you return from a helper function in Dust it causes output to be written to the stream.

osuushi commented 11 years ago

I largely agree, but there is at least one situation where I'd hate to see it go, which is one-liner functions. I most other cases, though, I find that they end up simply being a way for me to make a simple error that has no immediate visible effect on the program, but has a cumulative impact on performance. Even worse, if you're making a library and you accidentally return something in a function that should return nothing, your users might start using that return value, leaving you with the decision to make it an official part of the API or break existing code.

As far as updating old code to use explicit returns, it would be very easy to make a tool to do that automatically.

I'd suggest that if implicit returns are to go, we should have a nicer way of writing return, and I'd nominate <- which has a nice symmetry with the function marker. So

countRodents = (animals) ->
    count = 0
    for critter in animals
        count++ if critter.order is 'rodentia'
    count

would become

countRodents = (animals) ->
    count = 0
    for critter in animals
        count++ if critter.order is 'rodentia'
    <- count
devongovett commented 11 years ago

Yeah, not sure changing return syntax at the same time would be a good idea. return really isn't that long, and it is very clear what it does whereas something like <- is just one more thing to learn. Plus, one liners would look weird.

square = (x) -> <- x * x

vs.

square = (x) -> return x * x
osuushi commented 11 years ago

To be clear, I do not think that the <- syntax would work well for one-liners. I think they would only be a good fit if one-liners still had implicit returns.

vendethiel commented 11 years ago

Just as a side-note, coco brings "!" to suppress implicit returns

foo = !-> alert 'hi'
baz = !(x) -> alert "it's #{x.toUpperCase()}"

#or even
!function batBar
  doNothing a, b, c
michaelficarra commented 11 years ago

Absolutely not. This would go against most every design decision of CoffeeScript. Functions produce a useful value. When you define a function just to modularise a bunch of side effects, that's a procedure, and that should certainly be the extraordinary case. Almost every function you define in a CoffeeScript program should care about its return value. This is also true about functions in the jQuery case you provide, except that the value they return is unconditionally undefined. What's so wrong with specifying that? CoffeeScript is just trying to make it easier since, in the vast majority of cases, when execution hits the last statement of a function, the author would like the function to produce that value.

I can somewhat agree on the point about the value of a loop. One of CoffeeScript's greatest failings is that it does not distinguish between a loop and a list comprehension. This is a discussion for a different thread, but I'll at least state my stance on it here: comprehensions should have to be explicitly syntactically differentiated from looping constructs, whose value should be null or some more appropriate value -- but not a list.

jussi-kalliokoski commented 11 years ago

void ALL THE THINGS. ;)

devongovett commented 11 years ago

I disagree. Not all functions produce a useful value, and in fact most functions are actually procedures as you say and do not return anything. This is why in many if not most popular languages returning a value is opt in rather than required.

I don't see how this goes against every design decision of CoffeeScript. I see that CoffeeScript is trying to make things easier, but unfortunately in most cases it just makes code harder to read and more error prone than languages where returns are explicit. You end up working around the language when it doesn't do what you want, which in this case is most of the time. It just doesn't result in clear code.

A better example of the third point above is calling another function from within the jQuery each function, where you don't necessarily know what it returns. It could return false and break your code if you aren't careful to explicitly return nothing. This causes a whole class of bugs and makes CoffeeScript harder for newcomers, especially for those coming from JavaScript. CoffeeScript is suppose to have the semantics of JavaScript with a better syntax, and this is one of the rare cases where that isn't the case IMO.

This doesn't prevent you from returning things from functions, it just makes it more explicit, more readable and less error prone.

Can you give me examples where implicit returns are more clear than explicit ones?

On Aug 4, 2012, at 1:46 PM, Michael Ficarrareply@reply.github.com wrote:

Absolutely not. This would go against most every design decision of CoffeeScript. Functions produce a useful value. When you define a function just to modularise a bunch of side effects, that's a procedure, and that should certainly be the extraordinary case. Almost every function you define in a CoffeeScript program should care about its return value. This is also true about functions in the jQuery case you provide, except that the value they return is unconditionally undefined. What's so wrong with specifying that? CoffeeScript is just trying to make it easier since, in the vast majority of cases, when execution hits the last statement of a function, the author would like the function to produce that value.

I can somewhat agree on the point about the value of a loop. One of CoffeeScript's greatest failings is that it does not distinguish between a loop and a list comprehension. This is a discussion for a different thread, but I'll at least state my stance on it here: comprehensions should have to be explicitly syntactically differentiated from looping constructs, whose value should be null or some more appropriate value -- but not a list.


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

ScottWeinstein commented 11 years ago

in C# people.Select(x => x.Name) in CS _.map(people, x -> x.Name)

michaelficarra commented 11 years ago

Not all functions produce a useful value, and in fact most functions are actually procedures

That observation is subjective. It depends on your programming style and the application's requirements. When using a functional programming style, you never ever define procedures. When using an async style, passing around functions as continuations, it's a little more difficult to reason about them in the same way. I would consider callbacks neither functions nor procedures.

I don't see how this goes against every design decision of CoffeeScript.

CoffeeScript promotes a functional style with short lambda syntax, easy function application, and (almost-)everything-is-an-expression while still being friendly to the procedural developers. Most functions in a CoffeScript program would require an explicit return on the last statement if we made this change. I don't think that would benefit our users because we encourage that functional style.

I see that CoffeeScript is trying to make things easier

I wouldn't say it tries to make things "easier" in this case. I would say that it defaults to the most common usage. That might be easier for certain people in certain situations.

This doesn't prevent you from returning things from functions, it just makes it more explicit, more readable and less error prone. [...] CoffeeScript is suppose to have the semantics of JavaScript with a better syntax

It's not an "error" that you forgot to consider what the return value of your function should be. You just assumed it would default to the same thing it does in JS, which is a fallacy. This is not JS. What you're asking for is this: when execution reaches the end of a function without hitting an explicit return, the function has implied that it will return undefined at that point. CoffeeScript functions currently imply that they will return the completion value of the last statement. They're both implicit, but you claim that producing the useless undefined value is the more desirable behaviour. I would have to disagree on behalf of the majority of our users.

Can you give me examples where implicit returns are more clear than explicit ones?

It's not about clarity, as the implicit return value you propose is no more "clear" than the current semantics' by any applicable definition of the word "clear". Maybe it's more intuitive to you, but not to me or anyone who comes from a functional programming background.

andrewrk commented 11 years ago

See also #899 and https://github.com/satyr/coco/issues/91

Note that coco has syntax to suppress implicit return.

Most functions in a CoffeScript program would require an explicit return on the last statement if we made this change.

This is simply not true. Especially properly written asynchronous code has no return values, since the callback's arguments are the return values.

devongovett commented 11 years ago

Of course it's more clear. You can see immediately when looking at a function with a return statement that it will return a value and what value it will return. With an implicit style, you may have to hunt around. Why is it useful to have the return value of a function be the last thing it does? In the case of the jQuery example, you're assigning a variable to false. How is that useful as a return value? There is no distinction of when you actually want to return something from the function and when you did so "by accident" so to speak.

Looking through the CoffeeScript source itself, there are hardly any functions that actually make use of the implicit return values. Many of the functions don't write the word return but include the return value by itself at the end of the function, which is basically the same thing. Adding a return statement in front of that wouldn't be so bad, and would probably help highlight what is being returned. But why would you want the return value of a function to be the result of an assignment or another of the examples I mentioned above unless you explicitly told it to? I'm not seeing any place in the CS source or any other project that I've seen where that is actually useful.

Returning undefined by default is infinitely more useful. You don't "accidentally" expose internal functionality to the outside world by implicitly returning it when you forget to add a return at the end. You improve performance for functions with loops or comprehensions at the end where you might not want an array to be returned. undefined is sort of the unvalue value. It is a value that represents nothing, and so it is useful. For functions like the jQuery case that do something based on the return value, you only want to return something other than undefined when you explicitly want to do so.

This isn't JS exactly but it is in a way in that it interoperates with JS libraries in both directions - JS with CS source and CS with JS source. So it must play nice with JS and matching the semantics here is important I think.

CoffeeScript promotes functional programming, and it still can. It is likely that many CoffeeScript programs are already using sort of explicit returns, by putting the return value on the last line without explicitly writing the word return. The other functions that are not really supposed to return anything (or undefined as you would say) but don't because of a mistake of the programmer (assignments at the end of functions, loops, etc.) would simply be corrected to return undefined. Yeah, you'd have to write return at the end of your functions, but in my opinion that will make your functions more clear as to what they are returning, and since you likely already put your return values at the end of your functions without the word "return", it isn't really so hard.

andrewrk commented 11 years ago

Most functions in a CoffeScript program would require an explicit return on the last statement if we made this change.

I couldn't let this wild claim go unchecked, so I did some research. I picked a file at random from coffee-script's source itself: command.coffee.

I counted:

What gets me is that you say

Almost every function you define in a CoffeeScript program should care about its return value.

This is outrageous. The fact that coffee-script has implicit return causes programmers to not care about the return value, as I have just demonstrated above. This argument goes against what you stand for.

Conclusion: Implicit return should not be the default. Defining a function with a non-implicit return should require less than or equal to the same number of characters that defining a function with implicit return requires.

michaelficarra commented 11 years ago

The CS compiler is nowhere near a good example of a well-written or a functional program (or an async-heavy program that would support your point better).

The fact that coffee-script has implicit return causes programmers to not care about the return value, as I have just demonstrated above.

No, not at all. Functions (by definition) should always care about their return values -- the difference we're debating is in how it is specified. The current syntax allows you to more easily specify that the return value is the completion value of the final statement (because it is the implied value). @devongovett wants it to be easy to specify that the function produces undefined. If people are not considering the return values of their functions, they are either intending them to be used as procedures or they are making an error. For the third case, using a function as a continuation, I agree that the current implicit return semantics are not useful, but I would argue that the solution is to specify a call/cc differently, not to change how functions are specified.

Conclusion: Implicit return should not be the default. Defining a function with a non-implicit return should require less than or equal to the same number of characters that defining a function with implicit return requires.

You're both making an error in terminology and not reading my comments. Either way, the value is being implicitly specified. We are just debating what value should be implied. Quoting myself:

What you're asking for is this: when execution reaches the end of a function without hitting an explicit return, the function has implied that it will return undefined at that point. CoffeeScript functions currently imply that they will return the completion value of the last statement. They're both implicit, but you claim that producing the useless undefined value is the more desirable behaviour.

devongovett commented 11 years ago

Yes, glad you have understood what I am asking for. You still haven't answered why implicitly returning the result of an assignment, loop, or other case not involving an explicit return is more useful or more clear than implicitly returning undefined, as you have put it. I have explained why I think implicitly returning anything other than undefined is a bad idea. All I'm trying to do is save myself from writing return nothing lines at the end of many of my functions to work around a class of bugs related to the current behavior.

michaelficarra commented 11 years ago

I strongly believe that our users are defining functions with meaningful return values based on the calculations performed in their body, often generating the return value in the final statement of the body. I don't believe they often want to define functions that will produce an undefined value after performing some side effects. That's an uncommon practise in idiomatic coffeescript, and we'd like to make that slightly less convenient to encourage use of the functional paradigm.

devongovett commented 11 years ago

Speak for yourself. As this thread has shown, many users agree that this is a source of bugs and unexpected behavior. It sounds like you are excluding us as users of CoffeeScript. Please back up your claims and define "we" and "idiomatic coffeescript". People use CoffeeScript in many different ways, not just the way you personally use it. I've asked specific questions about why you think the current behavior of returning the results of assignments, loops and things from inside if statements is a good idea and given examples of the bugs that arise from this behavior. I'd like to see an example where this is truly useful. In those few cases where this is really what you want, it is easy to add a return to make things work as before, and adding it would make the code more clear anyway. Explicit returns are more clear because you know what the return value will be if you don't provide one: undefined. The current behavior is different for every function.

I'd really like to hear more opinions on this.

mstum commented 11 years ago

I think there is a slight fallacy in the C# example above: C#'s lambdas are pretty much the only thing that has an implicit return, and multiline statements require an block with an explicit return. I don't have enough experience in functional languages to say whether or not implicit returns are the exception or the norm (aren't real functional languages always only single line statements, curried together?), but unless there is a clear distinction between single-line and multiline statements, implicit returns seem like a smell to me. The only thing that should ever have implicit returns are single line statements IMHO, and there should be a clear distinction between single and multiline statements? Or, something that Delphi did, a distinction between a function and a procedure.

satyr commented 11 years ago

When using a functional programming style, you never ever define procedures

In practice you do. [unit -> unit](takes/returns nothing) is quite common in OCaml for example.

epidemian commented 11 years ago

I don't agree with this proposal.

I like that the general consensus is that it depends on what the general style of programming you're using though. If you are going for a functional style, implicit defined returns (as opposed to implicit undefined returns) are a cool thing, while if you're going for a messy very stateful style, they may not be so cool.

Now, CoffeeScript is trying to encourage a functional style, so it makes writing code in that style easier. I could write Haskell code in a procedural style, but i would be fighting the language every time; it would be easier to use a language that makes procedural code easier, or change my coding style to match what the language proposes. The same can go the other way if try to code in a functional style in Java.

There are other languages that also return the value of the last expression from functions; apart from functional languages, Ruby (which is very imperative) and Lisp are the first ones that come to my mind. In my experience, this convention made me think of empty returns as a code smell/warning. When i see one in CoffeeScript, i always think to myself "why is this function not returning anything? Can i do something to make this function more like a normal function and get rid of that ugly empty return?". It's like a goto or an empty catch =P

It can also be inconsistent when combined with conditional logic. For example:

 test = ->
   if someCondition
     doSomething()

This function returns the result of doSomething() only if someCondition is truth-y, otherwise it returns undefined.

I think this is a good example of why implicit defined returns are a good idea.

The question is why I would ever want a return value sometimes and not others.

I wonder the same. If i want the function to return a value, then there will always be an else case, because i want the return value to always be defined. I find myself writing code like this very often:

foo = ->
  if someCondition
    'some value'
  else
    'some other value'

Having to explicitly say that i want the value of that if expression to be the return value of the function is redundant.

On the other hand, if i want the value of the function to always be undefined, then writing the explicit empty return makes that intent... well... more explicit:

foo = ->
  if someCondition
    doSomething()
  return

It's usually not necessary to write that empty return though. When a function is only important because its side effects, then the last function it calls most surely also only important because its side effects, so it should also have an undefined return value; no need to write that empty return :)

Also, when a function is only important because its side effects, why would someone want to access its return value? :S

Finally, i think Coco's syntax for undefined-returning functions (or "procedures", as @michaelficarra calls them =P) is a cool compromise. The ! is kind of a warning: "hey, this function does not return anything, so it surely has some kind of side-effect, be careful!". Scheme uses the same symbol to express mutability as a convention; Ruby does something similar too.

jussi-kalliokoski commented 11 years ago

That observation is subjective. It depends on your programming style and the application's requirements. When using a functional programming style, you never ever define procedures. When using an async style, passing around functions as continuations, it's a little more difficult to reason about them in the same way. I would consider callbacks neither functions nor procedures.

I think you're generalizing your own flavor of functional programming as if it applies to every functional programming style. There are many different functional programming styles. If you want to refer to them in general I suggest you refer to a resource where this generalization is defined.

The fact is that not all functions return a value, even in functional languages. That's why there's void (not void *) in C. Some of the functional programming styles are also mixtures of object-oriented and functional style, as an example take a look at the czmq source. There the function may be more of a method that just does something to the object at hand and never returns anything. One could argue that those functions should return an error code, but not all functions have any points of failure. One could also argue that CoffeeScript is a functional language, but it's really more of a mixture, after all there are classes and objects.

In some styles a function is defined as something that does a simple thing and does it well, nothing to do with whether it actually returns something. It could do what it does to an object, or to an array.

Either way, your arguments are on thin ice if you base them on something supposedly definitive, namely functional programming style.

michaelficarra commented 11 years ago

I think you're generalizing your own flavor of functional programming as if it applies to every functional programming style.

You're right, sorry for that. You can clarify my previous statements a little by replacing all mentions of functional programming with "functional programming in a pure functional language".

jussi-kalliokoski commented 11 years ago

You're right, sorry for that. You can clarify my previous statements a little by replacing all mentions of functional programming with "functional programming in a pure functional language".

In that case, let me correct my statement: your arguments have their feet halfway in the water.

I'm pretty sure you know CoffeeScript isn't a pure functional language. Indeed, it's very object-oriented. So why you'd want to force principles/paradigms from pure functional programming style to it is beyond me.

michaelficarra commented 11 years ago

CoffeeScript has features that help users using many paradigms. The class syntax is for people that want a classical OO style. The :: operator is for those that want OOP while accepting JS's prototypal inheritance. And the short lambda syntax is for functional programmers. So shouldn't the functional programmers' favoured semantics be used?

jussi-kalliokoski commented 11 years ago

And the short lambda syntax is for functional programmers. So shouldn't the functional programmers' favoured semantics be used?

Sure, but not at the expense of others. The short lambda syntax is used by I'd bet everyone using CoffeeScript. So where implicit returns might please people favoring pure functional style (my style is very much functional, but I for one don't like implicit returns), does it offer anything useful or harmful for other people? 74.3 of all statistics are made up, but my guess is that pure functional style doesn't represent the majority of people using CoffeeScript. It's OK to have language features that a few use because they like them, but it's not OK if that choice is forced upon a majority that would rather not have it.

Regardless, a lot more data is needed to make a big decision like this, so more opinions (and rationales behind the opinions) would be helpful.

ccampbell commented 11 years ago

I have only recently started to try out CoffeeScript, and I like a lot of it. The implicit returns is one thing that has really started to drive me crazy though so I have to say I agree with this ticket.

I think one of CoffeeScript's goals is to make it easier to write javascript without really interfering with your code. For most things this is true. For example writing for thing in things is much simpler than writing for (var i = 0, length = things.length; i < length; i++), however when it comes to functions this is one area where it behaves the opposite of vanilla javascript.

When I write a function in javascript I expect it to have no return value unless I explicitly type return. Anyone reading the code will easily be able to see that in one function you are returning a value, but in another function you are not. CoffeeScript does the opposite which is not only unintuitive for people who are new to the language or reading the code, but can also lead to bugs as mentioned above.

It seems to me that this feature was added to prevent you from having to type return all the time, but I find myself having to type return even more often when using CoffeeScript than I do when I am writing regular javascript (to prevent implicit returns).

In the end I think removing implicit returns will cause the language to behave more as expected, improve code readability, and cut down on annoying bugs. I understand it's not an easy change to make cause it would probably break backwards compatibility with a number of projects, but I would vote for it.

phaedryx commented 11 years ago

For what it is worth, I really like implicit returns. I would like coffeescript less if they were removed.

epidemian commented 11 years ago

In the end I think removing implicit returns will cause the language to behave more as expected

I don't know. I think that's a very bold claim. At most, i'd agree that it would behave more as expected by programmers who are used to languages that require explicit returns.

For programmers that are used to the idea that a function is like a mapping from an input to an output, the idea of "returning" at some point doesn't seem so necessary. Consider the simple function:

double = (n) ->
  n * 2

I personally interpret that function as "the double of a number n is that number multiplied by 2". No need to explain it in terms of returning something at some point (i.e. as an imperative computation). That's why i think it reads better without a return there.

Yeah, yeah, i know: "but that's an unrealistically simple example!". But simple functions are very useful, and a lot of nice properties come from defining functions that are as simple as possible. We should always strive for simple functions.

When doing higher order programming, being able to define a simple function in a simple and succinct way is awesomely useful. So i really like that CoffeeScript lets me write areas = figures.map (f) -> f.area(). I think that forcing a return for all "valued" functions would make these kind of nested functions more confusing; if i'd have to write areas = figures.map (f) -> return f.area() at least to me, the question of "where i'm i returning from?" arises immediately.

Now, all this comes from the perspective that functions are like a mapping from an input to an output; something like in maths. And i do believe that, for the most part, subroutines (just to use a more computer science friendly word) should be functions.

But there are subroutines that are not functional in nature. Lots of languages don't distinguish these two concepts; they invent a void type, or an undefined value, and make procedures as a special kind of functions.

I think that rather than sacrificing the convenience of having a nice function notation for the special case of procedures, we could adopt a special syntax for those cases. Maybe a "shouted" arrow !-> (and it's this-binded fat relative !=>) like Coco's; or maybe some way to express "this expression's value is undefined" to put at the last expression of procedures (some kind of sigil i imagine... it should be more convenient than adding an empty return at the end).

domenic commented 11 years ago

I don't think anyone wants to remove implicit returns for single-expression functions. It's the multi-statement functions that it's problematic with.

epidemian commented 11 years ago

I don't think anyone wants to remove implicit returns for single-expression functions. It's the multi-statement functions that it's problematic with.

I think that would add more confusion than what it'd remove. If i have a simple function like:

endGameMessage = (score) -> if score > 100 then "You're awesome!" else "Keep trying dude..."

If i change it to a multi-line if for whatever reason (maybe i want to stick to the 80-characters-per-line rule, or i want to add more else cases, or add a more complex computation for the score before the result) then all of a sudden the function stops working because an explicit return is needed; there's no type-checker that'll warn me. I don't think that's very "expectable". And i'd prefer a more consistent rule for implicit returns: either always return the value of the last expression like CoffeeScript does now or always return undefined like JavaScript does; but not doing one or the other depending on the number of statements of the function.

domenic commented 11 years ago

@epidemian That would still be a single expression if you moved it to a second line.

epidemian commented 11 years ago

@domenic Yeah, if the rule is "one expression => implicit return" then yes, changing it to a multi-line if would still work. But, what would happen if i add another expression before the "You're awesome!" inside the truth-y part of the ìf? Would the if as a whole be a single expression still?

Or if i want to add an extra calculation before the if. This would work:

endGameMessage = (score) -> 
  if score > 100
    "You're awesome!" 
  else 
    "Keep trying dude..."

But this wouldn't:

endGameMessage = (score) -> 
  totalScore = computeScorePlusBonuses score
  if totalScore > 100
    "You're awesome!" 
  else 
    "Keep trying dude..."

I think that'd complicate the semantics of the language unnecessarily.

What do you think about adding special syntax for procedures, like !-> instead of ->? (or maybe another symbol, like ->>, i don't know).

domenic commented 11 years ago

What do you think about adding special syntax for procedures, like !-> instead of ->? (or maybe another symbol, like ->>, i don't know).

I guess that's probably the only way forward, especially given backward compatibility. I like ->> (less reaching across the keyboard for one).

It'd be a bit annoying to switch my write-a-function muscle memory from => to =>>, and to remember to switch back to => when I want simple single-expression functions, but it's probably the best alternative.

gampleman commented 11 years ago

Implicit returns are probably my 1. favorite feature of CoffeeScript. I like how it promotes thinking about functions in a more fundamental way rather then being just lumps of code.

I usually don't care if a function shouldn't return a value, it does anyway: then I simply don't use it. If I don't use it then typically Closure compiler will simply remove the unnecessary return. If someone uses my API and uses an undocumented return value, then they shouldn't be surprised that it breaks underneath their feet in the next version - that's why you should only use the documented stuff. The only real problem I see in this whole discussion is performance degradation when doing a loop at the end of a function and not returning anything. That's a pretty simple thing to keep in mind and IMO certainly not worth having to type return in every single function.

vendethiel commented 11 years ago

That's not. Another example is jQuery : you should always check you're not returning false, else you may break an event or a loop.

devongovett commented 11 years ago

@epidemian @domenic For the types of functions you are talking about, I really don't mind implicit returns. Where they are a problem is when you implicitly return the result of an assignment or the results of a loop as an array. In your example, even the multiline one, you put the return value by itself at the end of the block. Why else would you put a string on a line by itself at the end of a block? If the example function had been something like this:

endGameMessage = (score) -> 
  totalScore = computeScorePlusBonuses score
  if totalScore > 100
    showMessage "You're awesome!" 
  else 
    showMessage "Keep trying dude..."

I don't think you would want to return the results of those showMessage calls implicitly, as they could be anything and break your program somewhere else. Another example would be assigning those messages to some variable. You probably usually don't want to return the result of that assignment by default as well. And it is easy to add an explicit return if you did.

I really don't think adding another function syntax is a good idea. We already have two and I think that would just confuse things.

My first solution to the problem was just to make explicit returns required everywhere. A few people seem to be objecting over the extra typing. I don't think it's that much, but the other solution would be to simply change the rules on what gets implicitly returned and what doesn't. If that is a solution people feel better about, here's the rules I'd use.

  1. Results of assignments do not get implicitly returned. You can still put a return in front of the assignment to get that behavior.
  2. Results of function calls do not get implicitly returned. You can still put a return in front of the function call to get that behavior.
  3. All other expressions and values get returned implicitly (unless someone thinks of something else that shouldn't get implicitly returned). You can still use explicit returns if you like that. :)
  4. Loops, if statements and other blocks follow the same rules above such that if the last line of the block is an assignment or function call it does not get implicitly returned directly or as an array. You can still assign the loop or if statement to a variable and return that, or just put a return in front of the if statement or loop to get the old behavior. For example, this function would return an array as usual, since the last line of the loop is not an assignment or function call:
square = (arr) ->
      for x in arr
        console.log x
        x * x

The following function would not return an array, since the last line is a function call:

each = (arr, fn) ->
      for item in arr
        fn item

If you wanted the old behavior for that last function, you could write it like this:

each = (arr, fn) ->
      ret = for item in arr
        fn item
      ret

I think this compromise would work. You would continue to get the implicit returns where you want them, and we would avoid the bugs that result from places where you don't. The only change would be that the results of assignments and function calls (and their cousins inside loops, if statements and other blocks) would not get implicitly returned, but you could still get the old behavior by adding an explicit return. What do you all think of this solution?

epidemian commented 11 years ago

For the types of functions you are talking about, I really don't mind implicit returns.

The problem is: how do you differentiate those functions from the ones that would implicitly return undefined. The set of rules you propose make add some inconsistencies that you may not be aware of.

Results of assignments do not get implicitly returned. You can still put a return in front of the assignment to get that behavior.

The problem with this is that assignments are expressions in normal CoffeeScript. They can be used anywhere where an expression is valid. E.g. in another assignment:

index = total = 0

In the condition of an if statement:

if (index = str.indexOf something) isnt -1
  console.log "#{something} found at #{index}"

Really, anywhere where an expression is valid. So, to me, the expectable behaviour is that it would also be valid at the end of a function, as a other expressions, like literals or variables. A very simple refactor like memoizing the result of a function:

# Changing this...
foo = (bar) ->
  doHeavyComputation bar

# ...to this:
fooCache = {}
foo = (bar) ->
  fooCache[bar] or= doHeavyComputation bar

Would break. And finding that the problem is not in the new memoization logic, but the function simply stop returning the expected value, could be a potential PITA.

Results of function calls do not get implicitly returned. You can still put a return in front of the function call to get that behavior.

Then this original premise is not so true:

For the types of functions you are talking about, I really don't mind implicit returns.

As one of the examples i gave was areas = figures.map (f) -> f.area(). Unless you don't consider a method call the same as a function call, in which case that would be a new special case for the set of rules of implicit returns.

Also, the same argument that a function call is an expression and should be usable anywhere where an expression is valid also apply.


If the example function had been something like this:

endGameMessage = (score) -> 
  totalScore = computeScorePlusBonuses score
  if totalScore > 100
    showMessage "You're awesome!" 
  else 
    showMessage "Keep trying dude..."

If the example function would be something like that then it wouldn't really be a function. It'd be a procedure. One of the alternative proposals is to have special syntax for those cases to prevent them from returning any value.

However:

I don't think you would want to return the results of those showMessage calls implicitly, as they could be anything and break your program somewhere else.

I don't see the problem of it returning the result of showMessage. Why would it break my program somewhere else? Why would i'd be accessing the result of a function that's suposed to not return anything? In any case, it'd be an error in the caller code, not in the definition of endGameMessage.


So, in conclusion, i think adding all these rules for implicit returns is a net loss in consistency and simplicity of the language. I'd rather have implicit undefined returns always. But, as i said, i prefer the current behaviour even better :)

devongovett commented 11 years ago

@epidemian thanks for the response. I was worried it might be too much magic, and I too prefer requiring explicit returns always but I just wanted to see if a compromise could be made. I know that assignments and function calls are expressions, but I thought maybe these rules could help. It is probably better to have all or none.

Anyway, here's my response to what you had to say.

You could fix this example:

fooCache = {}
foo = (bar) ->
  fooCache[bar] or= doHeavyComputation bar

with an explicit return:

fooCache = {}
foo = (bar) ->
  return fooCache[bar] or= doHeavyComputation bar

Although the original function without the caching behavior would also not return anything without an explicit return under the proposed rules.

I don't see the problem of it returning the result of showMessage. Why would it break my program somewhere else? Why would i'd be accessing the result of a function that's suposed to not return anything? In any case, it'd be an error in the caller code, not in the definition of endGameMessage.

The reason it could break your program lies with the jQuery example given in the original issue. If you had a function like this:

$.each messages, ->
  doSomething this

And sometimes doSomething returned false, it would stop your loop perhaps without you knowing why. That's why implicit returns can be dangerous, especially when calling another function or assigning a variable. You end up having to code explicit returns of undefined manually all the time just to be sure your code won't break, which is really sucky and what this issue is all about.

If the example function would be something like that then it wouldn't really be a function. It'd be a procedure. One of the alternative proposals is to have special syntax for those cases to prevent them from returning any value.

In JavaScript and CoffeeScript, there is no distinction between functions and procedures. I really don't think having another yet function syntax is a good idea, and people would probably forget and use the wrong one anyway and we'd be back at square one.

So yes, I have always thought that explicit returns should be required all the time, but I was trying to come up with a compromise since people seem to be lazy enough not to want to type it out. Personally, I always use explicit returns, and I think it makes my code clearer. The compromise was an attempt to get the best of both worlds for those who like implicit returns, but it is probably better to go all explicit. I'd still like to hear what others think, however.

andrewrk commented 11 years ago

Here's my ideal scenario:

-> / => : implicit undefined --> / ==>: implicit last expression

Implicit last expression is longer because it occurs 1 / 5 of the time, and when you do want to use them, the code is short enough that this is still helpful.

I would still be satisfied if those were flipped.

Coco's syntax for implicit undefined unfortunately puts the suppressor far away from the -> which makes it cumbersome to type and modify. It would be nice if coco could move the ! so that you could always type !->. I've seen 3 people mistake the syntax for this which is a hint that this is how it should be anyway.

devongovett commented 11 years ago

No more function syntaxes! There is no need to have both implicit and explicit returns. It will just make the compiler more complex internally, and confuse people learning the language. It really is not a good idea. I like that people are trying to make compromises here (even I am), but introducing more syntax is not the solution to this problem in my opinion.

Implicit returns are a "nice-to-have" feature: not necessary, but they can make your life easier. Unfortunately, they also have side effects, like the bugs I have mentioned. I know this change will make your life slightly harder sometimes since you may have to type "return", but it will save you time debugging and prevent silly logic errors, while at the same time making your functions more clear and predictable as to what they are returning and exposing to the outside world.

jmreidy commented 11 years ago

In my experience, implicit returns have proven to be more trouble than they're worth. The time gained from saving keystrokes is certainly lost again when trying to debug one of the little issues that @devongovett has already pointed out. And that pain is magnified when you're managing a project with new CoffeeScript developers, who don't fully grok the ramifications of implicit returns, and the dangers of combining implicit returns with loop constructs.

While I find implicit returns to be a potential danger and less expressive than their explicit counterparts, there's one area where implicit returns are extraordinarily helpful: enumerable operations. In short functions like map callbacks, implicit returns allow for much cleaner, clearer code. In practice, I use explicit returns for everything but these types of calls.

Since implicit returns do have their use, instead of simply removing them from CoffeeScript (a change which would be extraordinarily painful), I'd recommend that explicit returns be incorporated into a linter like CoffeeLint.

vendethiel commented 11 years ago

We could choose to return implictly if the last expression isn't a function call

andrewrk commented 11 years ago

@Nami-Doc: @devongovett already suggested that. That behavior is too inconsistent. Compromises don't work in language design.

johnfn commented 11 years ago

No more function syntaxes! There is no need to have both implicit and explicit returns. It will just make the compiler more complex internally, and confuse people learning the language. It really is not a good idea. I like that people are trying to make compromises here (even I am), but introducing more syntax is not the solution to this problem in my opinion.

I disagree on both of these points. Making the compiler more complex internally should never be a reason to not do the right thing.

As for your point that this confuses users of the language, I don't really buy that either. I think that, for example, the difference between the fat arrow and the thin arrow is a lot more subtle to new language users than an implicit or explicit return marking. As with the the fat and thin arrow, decisions should be made that benefit the language as a whole.

I personally think that the distinction could be made a bit more obvious, say implicit (x, y) -> console.log(x + y), since 4 different arrows is a little confusing. However I don't think that extending the language slightly is an idea that should be tossed out without further thought.

The benefit of the implicit syntax is clarity and concision. Slapping returns all over the place makes most coffeescript code less concise and therefore less readable. Adding an implicit modifier makes a little bit of coffeescript code different, but not less concise.

devongovett commented 11 years ago

Don't confuse conciseness with readability. Writing out "return" improves readability by clearly marking what is returned. In the following function, there are two blocks with last lines containing a single value. The first one, inside the if statement, references window but nothing happens with that one. The second one causes the function to return 5.

test = ->
  if condition
    window
  5

IMO, the following is much more clear which thing is getting returned. You can figure it out by reading the whole function and granted this is not a particularly complex example, but just looking at these two functions, it is much clearer to me in the following version what will be returned.

test = ->
  if condition
    window
  return 5

What if when the condition is true you actually intended to return window, not just have it on a line by itself for no reason? With the explicit return marked, this is very easy to spot. With the original implicit version, it was much less clear.

Currently, CoffeeScript only requires explicit returns when you want to return early from a function, which is inconsistent and you end up with returns in some places and not in other places. Anyway, I'm just pointing these things out. Take them as you will, but IMO (obviously) explicit returns everywhere is better in almost every way.

jashkenas commented 11 years ago

whew -- big conversation. Feel free to keep the discussion rollin' in.

... but for what it's worth, this isn't a behavior we're going to change. CoffeeScript, more than being object-oriented, or function-oriented, is expression-oriented. The single most important thing that we try to change about JavaScript semantics is to make every piece of the language be a useful expression. Function calls are an important part of that.

Going back to the (not-so-representative) example from earlier in the thread:

  • 8 instances of implicit return being helpful
  • 40 instances of implicit return being applied to a function which was not meant to return a value
  • 2 instances of unintentionally returning the results of a for loop
  • 1 instance of explicitly using a return statement at the end of a function body to avoid this

... that tells me that even in a procedural codebase, implicit returns are important 16% of the time, and are harmless 95% of the time. There's a couple of cases where you may want to optimize to avoid returning the result of a comprehension ... but in command.coffee it doesn't make a tangible difference.

With the current behavior of explicit return when you don't want to use the value of the function, we get the clean semantics of an expression-oriented codebase, and the ability to optimize where you really need to.

devongovett commented 11 years ago

@jashkenas I had a feeling you'd say something like that. ;) Eh, it was worth a try I guess. Just wondering what you think about the jQuery bug in the original issue. This isn't just about optimizations, it can be the cause of bugs if one isn't careful. Of course, we can write explicit returns of undefined to avoid problems like this, but the issue was just about letting the language help out by not returning things you probably don't need and may cause problems (assignments especially). I usually put a return at the end of all of my functions that shouldn't return anything to avoid these problems, and simply wondered if the language could help me avoid doing that.

Thanks everyone for your input. This is an interesting discussion.

andrewrk commented 11 years ago

@devongovett I would like to extend you a personal invitation to coco land, where you can use !-> for every function declaration like I do. You won't regret it :-)

vendethiel commented 11 years ago

Hey, hey, we can keep the discussions without saying "use the same tool as me, it's the best" ;). and yeah, great inputs

jashkenas commented 11 years ago

I think that the occasional jQuery bug shouldn't put a kibosh on the whole idea.

Furthermore, why would you be using jQuery.each in CoffeeScript?

for item in arr
  map[item] = false

... and even if you did want to use it -- jQuery.each is a classic example of a poor API. Not only is the incorrect order of arguments a major bugaboo, but returning false to indicate you want to avoid the loop ...

jQuery.each(iterator)   # Where `iterator` is a function that may or may not return `false`

... means that you can't use the same higher-order functions in a map, where false is a perfectly valid value to return.