jashkenas / coffeescript

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

[IDEA] "returning" #332

Closed andreyvit closed 14 years ago

andreyvit commented 14 years ago

A well-known Ruby idiom is to replace

data: []
.... some code that fills in data (that cannot be rewritten as a comprehension)...
return data

with

returning [], (data) ->
    ... code using data...

While this can be expressed as a custom function, the resulting solution looks ugly. What about adding a "returning" directly into CoffeeScript? Like this:

returning [] as data
    data.push(42)
    data.push(24)

Question is, would the author want to accept a patch for this?

timbertson commented 14 years ago

wow, I've never seen that before.

Looks like it's implemented by activesupport as an extention to Object, rather than an actual language thing. Which makes your middle snippet the equivalent (without requiring any language changes). It doesn't seem that ugly to me...

Personally, I don't see the benefit - but others may disagree

zmthy commented 14 years ago

You could probably drop the returning, and have [] as data

Data operations here.

become an expression (rather than the return expression), so that you could use it anywhere without it taking up a whole function. If it was the last thing in a function, it would still return as expected.

If this was a part of the language I would probably use it, but I agree with gfxmonk in that it's not really a substantial enough benefit to bother including it because you could just write what it actually means without much more code.

jashkenas commented 14 years ago

I'm also not a fan of returning in Ruby / Rails. Object#tap is a variant that has much more practical use, for enabling chained calls on objects that don't support it natively. tap, however, we can't support without extending core objects...

So, here's my case against returning. It stymies the usual way of determining a return value, by making it unhelpful to check the bottom of the block. Instead, you have to be familiar with the construct. The counter-argument is that this is precisely the point ... you're indicating in the code that you're running a number of distinct configuration calls against an object, or performing a number of side-effect procedures.

I would argue that most places where returning is used:

returning car
  car.open_door()
  car.enter passenger
  car.close_door()
  car.drive_to 'work'

Are a case of feature envy. That list of stateful transformations should be encapsulated within the car itself, like so:

car.drive_to_work passenger

So, for a feature that inverts the expected code flow, and is designed to enable poor patterns of building up state, I think this ticket is a wontfix. Feel free to disagree, if you have some really compelling examples of how it's used.

andreyvit commented 14 years ago

“Get of object from some function; do something else with it; return it” is a very general pattern found in nearly every piece of code. Say you're rendering a node:

renderElementWithChildren: (el) ->
    node: renderElement el
    for child in el.children
        $(node).append renderElementWithChildren(child)
    node

Or maybe you want to use a size and return it at the same moment:

applySize: (comp) ->
    size: ...computation...
    $(comp.node).css { width: size.w, height: size.h }
    size

Or maybe you want to write a function that wraps another function and adds to its behaviour:

renderAndApplyProperties: (comp) ->
    node: renderComponent comp
    $(node).css { ..... }
    node

Note that in most of these examples the returned value wasn't even being mutated, so the concern of encouraging mutator chains does not apply to my use cases (my bad I used push() to illustrate).

Now, writing “return node” or “node” at the end of a function strikes me as ugly and non-CoffeeScriptish. Maybe you don't often do that — good for you then, but it craps up my code often enough to be willing to cook a patch for this.

Now for “not really a substantial enough benefit” — isn't the goal of CoffeeScript to add little things that make coding more fun?

And I guess err.the_blog makes a good point for usage of “returning” even in mutator scenarios (http://errtheblog.com/posts/27-quickly-returning), like this:

def change_state(object_id, new_state)
  object = find(object_id)
  object.state = new_state
  object.save
  object
end

While sometimes your code benefits from defining an updateStateAndSave method on an object, sometimes it does not, and for those cases “returning” allows the code to stay clean.

I agree that in CoffeeScript it makes sense to just call this “as”.

andreyvit commented 14 years ago

Note that this could just be a variant of JavaScript's “let” statement:

https://developer.mozilla.org/en/New_in_JavaScript_1.7#Block_scope_with_let

Say if we support “let”, and its value is the value of the last assigned expression, does that sound like a better plan to you?

jashkenas commented 14 years ago

andreyvit: We're discussing this on #coffeescript at the moment, so feel free to pop in if you'd like to talk about it.

From the discusson on #coffeescript:

andreyvit commented 14 years ago

My samples rewritten with the proposed syntax:

renderElementWithChildren: (el) -> (node)
    node: renderElement el
    for child in el.children
        $(node).append renderElementWithChildren(child)

applySize: (comp) -> (size)
    size: ...computation...
    $(comp.node).css { width: size.w, height: size.h }

renderAndApplyProperties: (comp) -> (node)
    node: renderComponent comp
    $(node).css { ..... }

I'm quite happy with it. Too bad it seems to give you a hard time updating the grammar. Note that you can ditch single-liners w.r.t. to this feature — i.e. only look for (ident-list) NEWLINE.

timbertson commented 14 years ago

I still think it's mad introducing more syntax rules to learn, when the purpose of the vanilla coffee-script version is clearer, and almost exactly as verbose (maybe an additional line, maybe not)

jashkenas commented 14 years ago

After a bit more discussion, this enhancement seems like something we should either go one way or the other on. Named return values in the function signature should either be required or left out, and I don't think we can justify a forced use. If this were a statically-typed language where we had to define the return type every function in the first place, it would be different.

So, closing the ticket as a wontfix. It's not a common enough case or a persuasive enough pattern to justify a language feature, and returning is a function that you can write perfectly well on your own, like this:

returning: (object, block) ->
  block(object)
  object

And then use:

change_state: (object_id, new_state) ->
  returning find(object_id), (obj) ->
    obj.state: new_state
    obj.save
weepy commented 14 years ago

Out of interest - what was the reasoning behind "Named return values in the function signature should either be required or left out". Consistency throughout the language ?

timbertson commented 14 years ago

I pointed out that introducing them (but having them optional) would mean there are multiple places you need to look in order to figure out what's being returned - each return statement, and the function header. I think it should be one or the other, otherwise it gets confusing.

andreyvit commented 14 years ago

…AND Jash wanted to feel good about zero open tickets. kidding

Agreed, when you have to look at each return statement (esp. that one buried 3 indentation levels deep), looking at the function header makes quite a difference.

While at it, why don't we disallow multiple “return” statements to make figuring the return value even easier?

Now seriously. Named return values was going to be one of the beautiful twists of the language, documenting all those functions that return something which is not quite clear from the name (like find() returning a zeroBasedIndex). And you kill it why? Because it makes 0.1% more difficult to look up return values. Mind you, it makes figuring the return value unnecessary in many cases, since it's documented right in the header.

weepy commented 14 years ago

It does sounds pretty neat. I sometimes feel a bit wrong when I'm returning values from a function in what feels like a dirty way. It's probably one of those features that we'd all have to try in real coding to get a true idea if it was useful or not.

Here's a gist of some code that I'm actually working on with before/after return headers:

http://gist.github.com/380477

StanAngeloff commented 14 years ago

I have several concerns about this. But first, in my time working on Coffee I've learnt to accept the fact not every little (or big) feature is core material. Everyone is entitled to their opinion and as a community we have to respect that and just deal with it.

Now onto my concerns: if we adopt the header, what happens if we return another value in the code? Would that be considered an error? If it is not one, then how does it make code better if you still have to scan the entire body for returns?

Do we completely drop the return statement otherwise? What about all those folks coming from JavaScript -- it's one more step on the learning curve; not a huge one, but one more nevertheless.

How about functions that return a value from multiple places? A trivial example:

fn: (cond) ->
  if cond > 10
    do_work()
    return 1
  if cond < 10
    do_other_work()
    return -1
  do_work_when_cond_less_eq_0()
  return 0

fn: (cond) -> (result)
  if cond > 10
    do_work()
    result: 1
  if cond < -10
    do_other_work()
    result: -1
  if cond >= -10 and cond <= 10
    do_work_when_cond_less_eq_0()
    result: 0

I don't consider the rewrite better, do you? If we adopt headers, how do we stop, i.e., exit a function? Think the above example with several nesting levels and returns in a lot of places.

We also have to consider one-liners, those lovely short functions that just make sense:

# which one is it?
map list, (n) -> n * 2
# or?
map list, (n) -> (v) v: n * 2

# would this be valid syntax?
map list, (n) -> (n) * 2

Also take a look at weepy's example. I may be wrong, but it saved him three lines of code in total? I am still living in Coffee land and I find the reworked copy confusing as I expect the last line to be returned ..

It's probably one of those features that we'd all have to try in real coding to get a true idea

I admit, I like what headers can bring to the language, but without solving the issues it would create first, it is not ready for prime time.

andreyvit commented 14 years ago

Here's my vision on this: named return values are meant for those functions that look better with them, and not meant for all other ones.

The functions that benefit from named return values likely have just one return at the end of the function, and look more or less like this:

func: (input) -> (output)
    output: otherFunc input
    console.log "Output was ${output}"
    yetAnotherFunc(output)

I'm okay if we completely ban “return” statements inside such functions, if that helps to get the feature landed. However, I can easily imagine the following being useful either:

func: (input) -> (output)
    return nil unless somethingHoldsTrueFor input
    return nil if theresAnotherWeirdThingWith input

    output: otherFunc input
    console.log "Output was ${output}"
    yetAnotherFunc(output)

So I'd vote for not banning returns, and just have them override the implicit return values. But I would be just as happy with banned returns.

To all those guys who worry about looking for return statements in a function: just don't use named return values for obscure functions. Again, this feature (like any other one actually) is to be used only when it makes the code better and clearer.

I guess this falls under the general use/abuse discussion, though I believed those who want to see abusable features banned usually stick to statically typed languages. The key is that you really don't have to abuse named return values (or destructuring assignments, or statements-are-expressions, or dynamic typing, or lambdas).

What about adding this as a provisional feature, without any promises for backwards compatibility, and give it a try? (Though CoffeeScript's home page makes it clear that the whole language is provisional.)

jashkenas commented 14 years ago

Ah, but just as the language itself is provisional, so is the "closed" state of this ticket. Re-opening it because there's still plenty of discussion.

timbertson commented 14 years ago

I worry about all too many provisional features, because it's hard to take them out if someone (even a vocal minority) like them.

To all those guys who worry about looking for return statements in a function: just don't use named return values for obscure functions. Again, this feature (like any other one actually) is to be used only when it makes the code better and clearer.

Every feature adds to the surface area of the language. A larger surface area means more you need to hold in your head to read code, and more things to learn (and forget) when you're just starting out. If, while learning the language, you miss some part of it, then you could get completely stumped by this. Especially if it's a syntax-only thing like this - no keywords to google or look for in documentation.

Perl says "there's more than one way to do it". Python says "There should be one-- and preferably only one --obvious way to do it". I'm obviously in the python camp. I think ruby and perl have far too many odd constructs and "beautiful twists", as you put it (I do not want to deal with a twisty programming language).

I believe this construct adds very little to expressivity or conciseness, and adds the potential (however minor or trivial it seems in discussion) for confusion to one of the most important parts of a function to be able to understand at a glance - what it's actually giving you!

weepy commented 14 years ago

It's a pretty easy concept to learn. Granted it won't save many lines of code, but once you understand the idea, it makes quite a few scenarios easier to understand.

andreyvit commented 14 years ago

Oh yeah, now it makes sense: I'm definitely in the Ruby camp here. I think I'll spare the arguments (for “more than one way to do it”), since the discussion obviously can't lead to an agreement.

So we need to find a way to agree without really agreeing.

My view of the facts is:

  1. There's a fair amount of functions that can avoid being terminated with “varname” or “return varname” by using this feature. Seems like no disagreement on the “fair amount” part.
  2. Some (non-trivial) number of people consider nr. 1 to be an important improvement. Some people (including the creator of the language) consider nr. 1 to be irrelevant.
  3. Adding this feature makes the language slightly larger, slightly harder to learn, and slightly harder to read.
  4. Some people consider nr. 3 to be important. Some people consider problems in nr. 3 to be imaginary, and disagree with “harder to read” part completely.
  5. This feature does not break existing valid code, unless it's weird enough. That “unless” part depends on the exact syntax we require. If we require a newline+indent after the parameter name declaration, I'm not even sure it can break the existing code even theoretically.
  6. Whether we include this feature or not, in any case it's unlikely to drive someone's decision to use or not use CoffeeScript.
  7. People who want this feature will continue to whine occasionally unless it is implemented. People who don't want this feature are likely to not whine after it is implemented. :)
  8. By default (as it is now), the feature is not included. :)

Now some judgmental claims. If we were talking about adding generics, or type inferencing type system, or actor-model concurrency, it would make perfect sense to talk about the surface area, and being harder to learn etc. But seriously, a named return value? That's totally trivial. The docs can describe it in two sentences. That's not something that gives you a headache for keeping it in your head.

Man, limitations of discriminant constraints of anonymous access types in Ada — that's something that's hard to keep in mind (AND learn). Names for return values — not worth an argument about learnability.

StanAngeloff commented 14 years ago

@andreyvit: on 7. and 8. -- what I would do -- I want something badly. I add it to my own branch/fork. I let the community and Jeremy decide if it's core material. If it's not, hey life goes on. I keep on using it nevertheless. Stan is a happy bunny.

So we need to find a way to agree without really agreeing.

Sounds like a plan, LOL.

EDIT: @Tesco:

You haven't experienced the community response to the half operators, then.

Could I just say I don't like them? Consider this whining.

zmthy commented 14 years ago

People who don't want this feature are likely to not whine after it is implemented.

You haven't experienced the community response to the half operators, then.

I feel like the decision on this one is going to be an indicator for future development for the language, so it's important that it meets the spirit of the language. The community seems fairly equally split on the issue, as do the arguments for and against. Unfortunately for the group who are trying to get it into the language, it seems like most of the current contributors to the code base are against it or are on the fence.

How many lines of code it saves seems to have come up a lot, but I think that's almost irrelevant. What's important here is clarity, and that's what the arguments should, and have for the most part, focus on. In one camp (and the original viewpoint when the idea was originally discussed) is that it defines a return value immediately, making it very clear what your intentions are with that value throughout the rest of the function. In the other, there's the fact that a return statement might overwrite this value, leading to less clarity rather than more.

There's also the case of whether it should be compulsory for every function. I think this is a terrible idea illustrated in some examples above, as we lose the ability to define lamba-like functions. Making it compulsory for block functions makes more sense - you're defining a lambda function with side effects. Rather than defining simply return values, you might even write this: (x) -> x * 2 puts "Times two!" This still suffers from the above contraints about return, and it was suggested that we make them invalid in a function definition like this. Why this is a bad idea has also been discussed above. Compulsory isn't really an option, and if the consensus is that it should be all or nothing (which it isn't, of course), then it simply isn't a valid proposal.

Maybe my brief proposal above makes more sense as an optional part of the language, with that kind losing the ability to return a different value? I'm not really concerned, as long as a decision is not made too hastily, either for or against. I like the idea, and I'd hate for the final decision to negatively affect the rest of Coffeescript's development. Like Stan said, build it for yourself, try it out, and come back with the implementation.

andreyvit commented 14 years ago

Talking with Jash more about this, we came up with more cases where this feature helps in case it is allowed.

# chainable method
foo: (args) -> (this)
    doSomething()

# reduce function, which naturally often returns one of its arguments
foo: (sum, item) -> (sum)
   sum.add(item)

# stop the function from returning its last statement
buttonClicked: (evt) -> (undefined)
    alert "Clicked"
    someOtherFuncThatMightAccidentallyReturnFalse()

# returning a known literal from a function
tryProcessingSomething: (something) -> (true)
    doStuffThatCannotFail(something)

Not sure all of these use cases should be supported (to me the “undefined” one looks compelling, while “true” one looks nice but not too important), just an idea.

StanAngeloff commented 14 years ago

Good examples -- however still unclear about short lambdas, e.g., fn: (n) -> n * 2? How would you go about writing this? Another case:

invert: -> map arguments, (n) -> n: * -1
andreyvit commented 14 years ago

I think I'm entirely for allowing only valueless return's in functions with named parameters:

foo: -> (boz)
    return if currentItem is null  # okay
    return 42 if ultimateQuestionAsked()  # BAD
    boz: computeBoz()
    configureBoz boz

This works for quick checks when you want to return a falsy value:

updateSize: (comp) -> (size)
    return unless comp.isInDocument  # returns undefined (actually would prefer null here...)
    size: computeSize comp
    $(comp.node).css { width: size.w; height: size.h }

but then you can't forget to return something that you've promised:

foo: (args) -> (this)
    return unless areCoolEnough(args)  # can't accidentally say "return null" here
    ...
andreyvit commented 14 years ago

StanAngeloff: you just don't use this in lambdas.

andreyvit commented 14 years ago

For the record, I'm not saying we can't think of some better syntax for this.

weepy commented 14 years ago

surely the rule is simple: named returns look like a variable name in parentheses after the ->. So fn: (n) -> n * 2 is as you expect.

jashkenas commented 14 years ago

andreyvit and I had a nice long chat about this on #coffeescript. I'm closing this ticket again as a wontfix, and here's why:

The great appeal of this proposal lies in the function signature, the possibility that at a glance you would be able to read the inputs and the outputs of a function. But upon closer inspection, it's a false appeal.

Reading through a typical CoffeeScript function begins with the function name, followed by a list of inputs to the function, and then the function body, and finally, at the end of the function body, the output value.

This is the proper order in which to read a function -- you need to know the inputs in order to understand the body, and you need to read the body in order to understand what the result value is.

The proposal in this ticket names the result value, but you have no basis for knowing anything about the value without reading the method body, and, within the method body, you have no way of knowing at what point you can stop reading, and the point after which the rest is just side effects.

With the current way of doing things, there's no such problem. Everything until the end of the method is always relevant to the return value, and the last line of the function is always the return value, whether returned explicitly or not. This is far better, I think, then having to scan through the function body to find the last line that mentions the name in the signature, and then tracing backwards from there.

So, if we were to re-invent this named function result idea from first principles, I'd prefer to see it laid out in the proper order: input -> body -> output. Which brings us back to what we have right now. Closing the ticket.

yfeldblum commented 14 years ago

It appears I'm very late to the party. But I tend to agree with jashkenas here.

I favor rigor and consistency even in my high-level / dynamic / scripting languages. And that's the sense I get from phrases like "the proper order in which to read a function" and "from first principles."

This feature seems like it would be a convenience for a number of people. But I do not see it as permitting us to write code at a higher level than before. Like jashkenas, I see this as causing more confusion than clarification.

Conveniences in good languages are well-founded and well-grounded. And just like Steve Jobs does, a language designer must say "no" to a thousand good ideas in order to make room for and say "yes" to the next killer idea.

As a footnote, Haskell permits the style of programming wished for in this ticket:

add a b c = d
  where
    d = x + c
    x = a + b

So the feature is interesting and useful when it comes to Haskell, because program flow in Haskell is not linear, not input -> body -> output. But I don't think the feature integrates well into CoffeeScript.

defunkt commented 14 years ago

Just wanted to pipe in to say that returning is not a well-known Ruby idiom and was abandoned long ago by the community.

There was a brief moment in time where us newcomers, from PHP and Java and the like, thought, "Wow, look at the cool things Ruby can do!" So we used returning to show how serious our language was. But it wasn't really that serious. Different for the sake of different never is.

We don't define normal methods with define_method, create normal classes with Class.new, nor return values with returning.

I currently have 207 RubyGems installed and only two (2) of them use returning, not counting Rails or Facets.

Disclaimer: I wrote the referenced Err the Blog post on returning, and I haven't used it in probably three years.