jashkenas / coffeescript

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

Improve chaining syntax #1495

Closed JulianBirch closed 7 years ago

JulianBirch commented 13 years ago

Since chaining is a pretty common feature of Javascript libraries, how about a dedicated syntax for it?

e.g.

$("p.neat").addClass("ohmy").show("slow");

could be written

$("p.neat")..       # Note the "dot dot"
  addClass "ohmy"
  show "slow"

Alternatively, you could go with something insanely LISPy

->>
  $ "p.neat"
  addClass "ohmy"
  show "slow"

NB, the semantics I'm proposing would be equivalent to

a = $ "p.neat"
b = a.addClass "ohmy"
c = b.show "slow"
michaelficarra commented 13 years ago

I think this is a duplicate of #1251

JulianBirch commented 13 years ago

Yes and no. The problem is the same, but proposed solution is different. 1251 was rejected and closed because the syntax proposed ultimately didn't actually improve matters. I feel the above proposals do, in fact, produce a nicer syntax for "train wreck" expressions. It requires you to use separate lines and indentation, but if you wanted to do it on one line, you could still fall back to the existing syntax. I've realized the ".." syntax can be futher improved to

$ p.neat..
  addClass "ohmy"
  show "slow"

Which I feel looks a lot more like coffeescript than

$("p.neat").addClass("ohmy").show("slow")
satyr commented 13 years ago

Also see #1407.

devongovett commented 13 years ago

How about just putting the dots there as usual, but instead of this code:

$ "p.neat"
  .addClass "ohmy"
  .show "slow"

compiling to this JS as it does now:

$("p.neat".addClass("ohmy".show("slow")));

it would compile as expected to this JS:

$("p.neat").addClass("ohmy").show("slow");

This would essentially make the newline character mean something after function calls without parenthesis.

erisdev commented 13 years ago

@devongovett cute, but wouldn't that syntax make it harder to correctly parse this valid (and pretty common) construct?

lemmeTellYas 'all about some fruits'
    kiwi: 'real itchy-like'
    okra: 'all slimy inside'
    fig: 'full of little wasps'
devongovett commented 13 years ago

@erisdiscord no, I don't think so as the dots before the function names would be required and there aren't any colons after them. It's really the same as if you just removed parenthesis from around the arguments of each function.

JulianBirch commented 13 years ago

Whilst my preference for as little punctuation as possible is marked, I can't see anything unambiguous with Devon's rule.

arbales commented 13 years ago

+1 for @ devongovett's proposal — this might be the behavior I'd expect anyway.

TrevorBurnham commented 12 years ago

Y'all should take a look at #1407. The proposal there would allow you to write

$ "p.neat"
  .addClass "ohmy"
  .show "slow"

without making newlines/indentation significant. In my view, it's the most elegant possible solution to this long-standing problem.

qrgnote commented 12 years ago

A more explicit way perhaps could be

$ "p.neat" > addClass "ohmy" > show "slow"

or even with surrounding spaces

$ "p.neat" . addClass "ohmy" . show "slow"

currently writing all the () parenthesis is a pain.

I don't like the idea of using straight indentations for chaining. Indentations to me means "Sub/Below" but chaining is more like "Next"...

Something like

$ "p.neat"
.addClass "ohmy"
.show "slow"

would make more 'sense' but Jeremy already wrote that off here.

Maybe something like:

$ "p.neat" > 
  addClass "ohmy"
  show "slow"

could work.

I kinda like JulianBirch idea for .. as well...

$ "p.neat" .. addClass "ohmy" .. show "slow"

$ "p.neat" ..
  addClass "ohmy"
  show "slow"
tcr commented 12 years ago

Just to throw another syntax in the ring, the ellipsis operator could be overloaded to mean "continue with result of last line", as such:

$ "p.neat"
...addClass "ohmy"
...show "slow"

You're not really saving punctuation but it is very simple to write, just as easy to read.

JulianBirch commented 12 years ago

As qrgnote points out, jashkenas doesn't like the leading dots proposal. Satyr does, obviously, since it works in Coco. Personally, I think* that when writing normal javascript, it's more common to be writing ")." or "})." at the start of a line than just ".", which makes me favour changing the behaviour.

I don't think using "greater than" works, because it's impossible to tell what you meant by ">" anymore. Which actually pretty much leaves us with the ".." proposal.

*I remember than when Python introduced their inline if statement, they designed it on the basis of analysing a large code base (their own) to see what was the more common idiom. I did a quick scan of jQuery edge for ^\s+[.] which found only one case in which the "dot on newline implicitly closes a bracket" would not have been the intended behaviour.

For reference, the line was

            if (rvalidchars.test(data.replace(rvalidescape, "@")
        .replace(rvalidtokens, "]")
        .replace(rvalidbraces, ""))) {
qrgnote commented 12 years ago

It'll be great if it can work inline as well... because going back and adding (&) is annoying.

check = require "validator" ... check

could return

check = require("validator").check

Edit:

Actually in practice entering ... is weird inline... keeps me thinking of "to be continued", maybe > or . (spaces) would be best.

check = require "validator" . check
check = require "validator" > check
patrys commented 12 years ago

Why fix something that is not broken? Introducing new meaning to existing symbols and operators is risky as at some point the JavaScript itself could decide to overload them. And chaining calls is not a common idiom in either CS or JS. It's just something that one popular client-side library happens to use.

Ruby is fine without chained call support. JS and CoffeScript offer you working solutions that require you to be verbose. If you need something else, you can probably roll your own solution without modifying the language, see this Ruby example for an idea of what to add to jQuery's prototype:

http://stackoverflow.com/questions/4099409/ruby-how-to-chain-multiple-method-calls-together-with-send/4099562#4099562

JulianBirch commented 12 years ago

Underscore isn't the only popular library that uses method chaining. jQuery uses it as well. The reason I suggested the ".." syntax is because that's the syntax Clojure has for this, so it's useful in Java as well.

patrys commented 12 years ago

@JulianBirch, I agree but the idiom is still not part of the language philosophy per se and reusing operators that are already part of the language comes with the risk of them suddenly gaining a meaning in a future version of JS.

JulianBirch commented 12 years ago

Certainly, and tbh I think this has come down to a "won't change". I do find thrush operators useful, however, and it's a pity CoffeeScript doesn't have one.

banacorn commented 12 years ago
$ "p.neat"
  .addClass "ohmy"
  .show "slow"

this is exactly what I'm looking and expecting for. parenthesis is really a pain when doing chaining

qrgnote commented 12 years ago

yes, CoffeeScript does not support chaining.

You can't use CoffeeScript to chain, you have to resort to JavaScript.

CoffeeScript does such a great job of relieving parenthesesitis, that when I have to return to JavaScript awk!!! Especially when I have to "go back" to add parenthesis.

it's not easy to add parentheses backwards

my-module = require './mymod'
# awk! have to go to back and add parentheses
my-module = require('./mymod').method

It seems that not supporting chaining is an oversight of CoffeeScript. Chaining isn't just used by jQuery like the previous example shows, it's pervasive in CommonJS modules loading, and what's more Node, then that? Let alone a lot libraries like Underscore supports "chaining" as well. Like mongodb/mongoskin

posts = require('mongoskin').db('localhost:27017/blog').collections('posts')

jQuery and all the custom libraries built off of jQuery is important to support native imho. Parentheses sucks. I don't want to write JavaScript, I want to write CoffeeScript.

Question, why was "parentheses" made optional on function/method calls in the first place? That same rationale should be why chaining should be supported without parentheses.

chain 'uh'
chain('uh').chain "why does"
chain('uh').chain("why does").chain "my method calls"
chain('uh').chain("why does").chain("my method calls").chain "sometimes requries"
chain('uh').chain("why does").chain("my method calls").chain("sometimes requries").chain "parentheses"

Simple, CoffeeScript doesn't support chaining.

patrys commented 12 years ago

Consider me insane but I find foo = require('bar').baz much shorter and way more readable than:

foo = require 'bar'
    .baz

If parentheses are your number one problem in programming then let me congratulate you for having nothing else to worry about :)

erisdev commented 12 years ago

yes, CoffeeScript does not support chaining.

Wait, what?

You can't use CoffeeScript to chain, you have to resort to JavaScript.

Huh? This looks like CoffeeScript to me: $('.awesome').css(color: randomAwesomeColor()).appendTo('.gr8-stuff')

While I agree that a syntax that allows us to leave off parentheses would be the ant's pants, the status quo really isn't as bad as you make it sound. We've been using parentheses for years in other languages. C:

(If you're allergic to parentheses, you'll want to be extra careful and avoid exposure to Lisp or S-expressions)

banacorn commented 12 years ago
$('#blah')
    .bar('foo')
    .bar('foo')
    .bar( ->
        fn()
    ).bar 'foo'
$ '#blah'
    .bar 'foo'
    .bar 'foo'
    .bar ->
        fn()
    .bar 'foo'

I think the latter would make my day better :)

jashkenas commented 12 years ago

Food for thought from @raganwald: http://news.ycombinator.com/item?id=3175028

raganwald commented 12 years ago

The substance of my comment onHN:

I would use indentation to discriminate between the cases:

list
  .concat(other)
    .map((x) -> x * x)
      .filter((x) -> x % 2 is 0)
        .reverse()

For pipelining, and:

brush
  .startPath()
  .moveTo(10, 10)
  .stroke("red")
  .fill("blue")
  .ellipse(50, 50)
  .endPath()

For what we are calling “chaining.” I use this now as a personal coding style when writing jQuery stuff with JQuery Combinators:

added
    .addClass('atari last_liberty_is_' + ids_of_added_liberties[0])
    .find('#'+ids_of_added_liberties[0])
        .when(doesnt_have_liberties)
            .removeClass('playable_'+added_colour)
            .addClass('no_liberties');

The first “addClass” and “find” are both sent to “added,” “when” is sent to the result of “find”, “removeClass” and the second “addClass” are both sent to the result of “when.” It feels to me very much like how we indent scope and syntactic blocks like “if” or “unless."

erisdev commented 12 years ago

@raganwald :+1: That is precisely how I feel it should be as well, if CoffeeScript is going to have such a syntax. Lately I tend to write my chained method calls on the same indentation level as the object

brush
.startPath()
.moveTo(10, 10)
…

but I think that indenting it your way makes more sense and is ultimately more consistent.

qrgnote commented 12 years ago

Consider me insane but I find foo = require('bar').baz much shorter and way more readable than:

 foo = require 'bar'
  .baz

yes, but a single line chaining syntax would be nice.

foo = require 'bar' . baz
# or
foo = require 'bar' > baz
foo = require 'bar' .. baz
foo = require 'bar' ... baz

but @patrys point about future-proofing coffee-script is a good point... we just have to decide on a syntax.

my vote right now goes for .. or both...

brush 
  ..moveTo 10, 10
  ..stroke "red"
  ..fill "blue"
  ..ellipse 50, 50

brush ...
  .moveTo 10, 10
  .stroke "red"
  .fill "blue"
  .ellipse 50, 50
raganwald commented 12 years ago

FWIW, Smalltalk implemented chaining in its syntax and was indentation agnostic, so you could write

foo
  bar;
  bash: something;
  thenBlitz.

or:

foo bar; bash: something; thenBlitz.

If you want to go that way, I would avoid .. as a potential readability and accidental mistype liability for the same reasons that = and == often trip people up in if statements. JM2C.

Unfortunately, the semantics I would like in an operator conflict with Coffeescript’s goal of compiling to readable JS that corresponds closely to the semantics of the original source. Instead of an operator that means “send and return the receiver,” I would like an operator that means “send to the previous receiver,” which is exactly what the ; in Smalltalk means. So instead of:

foo
  ..bar()
  ..bash(something)
  .thenBlitz()

I would have suggested:

foo
  .bar()
  &.bash(something)
  &.thenBlitz()

Meaning “send bar to foo, and send bash(something) to foo, and send thenBlitz to foo.” I fear that the compiled code would be confusing, but like the way it reads. It describes to me what the code is doing, not making me think about the implementation of returning the receiver and then sending a message to the result.

Like .., you could still use it on one line:

 foo.bar()&.bash(something)&.thenBlitz()
tcr commented 12 years ago

@raganwald +1. This seems like an intuitive, semantic distinction between chaining vs continuation of previous lines.

Moved comments about chaining parentheses omission to #1407.

tcr commented 12 years ago

Just to recap, it looks like we're discussing three problems:

  1. Multi-line chaining vs. pipelining syntax (sort of a with-like construct) using indentation or perhaps a new operator:

    $('body')
       .html('<h1>hey</h1>')
       .find('h1')
           .addClass('cool')
  2. Permitting multi-line paren-free chaining syntax:

    $ 'body'
       .html '<h1>hey</h1>'
       .addClass 'cool'
  3. A single-line chaining syntax using perhaps a new operator a la require 'foo' .. baz
satyr commented 12 years ago

@raganwald: Instead of an operator that means “send and return the receiver,” I would like an operator that means “send to the previous receiver,” which is exactly what the ; in Smalltalk means.

A separate proposal. See #1431.

TrevorBurnham commented 12 years ago

Thanks, @timcameronryan. Let me add that there is a separate open issue for that second problem, #1407.

So to clarify, @raganwald: Are you proposing a chaining syntax where function results are cached as needed so that you could write, for instance,

$('#todos')
  .addClass('rad')
  .children()
    .addClass('tubular')
  .parent()
    .addClass('dudetastic')

and have it be equivalent to

_ref = $('#todos');
_ref.addClass('rad');
_ref.children().addClass('selected');
_ref.parent().addClass('active');

? Or are you just suggesting a stylistic convention?

raganwald commented 12 years ago

If my dreams came true, then:

$('#todos')
  .addClass('rad')
  .children()
    .addClass('tubular')
  .parent()
    .addClass('dudetastic’)

Would compile to something like:

_ref1 = $('#todos');
_ref1.addClass('rad')
_ref2 = ref1.children();
_ref2.addClass('tubular');
_ref3 = ref1.parent();
_ref3.addClass('dudetastic’);

The obvious optimization is to take advantage of the fact that some of these ’nodes’ are degenerate and do not have more than one child:

_ref = $('#todos');
_ref.addClass('rad’);
_ref.children().addClass('tubular');
_ref.parent().addClass('dudetastic’);

This is exactly what @TrevorBurnham is asking. Presuming we have degenerate node optimization, this code:

list
  .concat(other)
    .map((x) -> x * x)
      .filter((x) -> x % 2 is 0)
        .reverse()

Compiles to itself:

list
  .concat(other)
    .map(function (x) { return x * x; })
      .filter(function (x) { return x % 2 === 0; })
        .reverse();
davidchambers commented 12 years ago

Trevor's example:

$('#todos')
  .addClass('rad')
  .children()
    .addClass('tubular')
  .parent()
    .addClass('dudetastic')

This is both valid CoffeeScript and valid JavaScript. While I see a great deal of merit in the proposal to make whitespace significant in such cases, the fact that a piece of code can be valid in both languages but mean different things is a cause for concern.

JulianBirch commented 12 years ago

Personally, I don't have much of an issue with flat indentation in either of @raganwald's examples. I don't do indentation like that when programming in Clojure, and I don't believe anyone else does either. It's well understood that each line (form) of -> operates on the result of the last one. If the last function happens to return its own parameter, that's fine, but not a cause for a different indentation strategy. Take a look a clj-webdriver for an extended exercise in using -> as a chaining syntax.

To clarify, what I was trying to set out to do at the start was propose a syntax that made chaining APIs easier to use than CoffeeScript, not provide a way of doing chaining-like things on things that didn't already support them.

If you just wanted to simplify the case in which the chain always returned the same object, VB6's with statement would do the job:

with $ 'p.next'
    .addClass "ohmy"
    .show()

I imagine most JS programmers are pretty allergic to anything containing the word "with", however...

Oh, and if I wanted to extract variables from a require, I'd have written

{bar} = require 'foo'

In the first place. :)

yuchi commented 12 years ago

Actually the concept behind with does not work well with standard chained code, while useful for other purposes.

# pseudo code
with $ 'div'
  .find 'span'
  .addClass 'span-element' # following `with` concept this should be applied to `$ 'div'`

should hypothetically yeld to:

var _ref = $('div');
_ref.find('span');
_ref.addClass('span-element');

and not:

var _ref = $('div');
_ref=_ref.find('span');
_ref=_ref.addClass('span-element');
patrys commented 12 years ago

What about:

with $ 'div'
  with @find 'span'
    @addClass 'span-element'

Not that it would make the code any shorter than JS.

raganwald commented 12 years ago

I like @davidchambers’s observation and agree that we should be concerned that:

a = [1, [2]]
a
  .pop()
  .pop()

and:

a = [1, [2]]
a
  .pop()
    .pop()

Are both valid JS with identical semantics. If Coffeescript is to have different semantics, it ought to be a big win for developers.

Tangentially, I find it surprising that Coffeescript is a language with “significant whitespace” but both of the above are valid Coffeescript and both compile to exactly the same code. If they don’t compile to different JS, I would expect Coffeescript to tell me that one of the two is illegal.

One of the benefits of a significant whitespace language is to use whitespace to save on things like braces and end statements. The other is to enforce a single consistent indentation across all code. Allowing both forms discards this second benefit.

I’m just ruminating here, I am not agitating for change. I’m flattered that you folks found my suggestion interesting enough to take the time to consider the ramifications, and I look forward to seeing how chaining might evolve into Coffeescript.

goomtrex commented 12 years ago

I like the raganwald era syntax.

Just wondering if it might support assignment:

Cuz = extends Bro, ( args... ) ->
  ...

::doStuff = ( args ) ->
  ...

.doThings = ->
  ...

Might go some way towards #1632, at least for class definition...

quangv commented 12 years ago
{bar} = require 'foo'

is the same as

bar = require('foo').bar

@JulianBirch cool didn't realize that, thanks. :)

showell commented 12 years ago

Does this proposal boil down to this?:

"Any statement starting with a dot implicitly operates on the value from its indentation parent."

If so, I'm -1. In cases where the indentation parent is far away, I think this will cause readability concerns. Also, I think many readers would expect the implicit object to come from the prior line.

I've only skimmed this thread, but it seems like nobody has put forward a concise explanation of what the rule would be. I understand that it's still a work in progress.

erisdev commented 12 years ago

@showell one could also argue against the indented if or for syntax using that same argument. It's not up to CoffeeScript to prevent bad programmers writing hard to read code.

showell commented 12 years ago

@erisdiscord Are you saying that CoffeeScript's design decisions don't impact code readability? Nobody with any sense of nuance would suggest that my arguments apply to if/for. That's just silly.

showell commented 12 years ago

The code below could have multiple interpretations:

    brush
      .startPath()
      .moveTo(10, 10)
      .stroke("red")
      .fill("blue")
      .ellipse(50, 50)
      .endPath()

The endPath() fragment could be acting on "brush", or it could be acting on the value returned from "ellipse". Obviously, only one interpretation would be valid under this proposal, but I think CS newcomers could reasonably expect either interpretation.

I'm all in favor of constructs that lead to terseness, but not if they introduce ambiguity.

erisdev commented 12 years ago

@showell I'm not saying CoffeeScript's design decisions don't impact code readability, but I think your argument is weak unless I'm misunderstanding it.

Vagueness and contrivance aside, how is either of these examples any harder to read than the other? In both cases, we end up with statements far from their indentation parent and readability is affected about the same IMO.

$ 'article'
  .applySomeStyle()
  .find 'h1'
    .makeNeatHeading()
    .addToTOC()
  .find 'p'
    .extractPullQuotes()

if entity?
  entity.updatePosition()
  unless 0 <= entity.x < width
    entity.x += width
    doSomething()
  unless 0 <= entity.y < height
    entity.y += height
    doSomething()

I think my sense of nuance is in working order, thanks.

erisdev commented 12 years ago

@showell .endPath() could apply to the return value of ellipse, sure, but under this proposal it wouldn't. There's no ambiguity because the meaning is well defined: a statement beginning with a dot applies to the result of its indentation parent. I would expect someone to learn the basics of the language if they intend to maintain code written in it.

This proposal is consistent with implicit object literals, BTW, where there's no confusion about what object andSoOn belongs to:

parentObject:
  childA:
    someGrandChild
    anotherGrandChild
  childB:
    yetAnotherGrandChild
    andSoOn

But this argument isn't the same as arguing that the distance from its indentation parent would affect readability, which is the point that I was addressing.

showell commented 12 years ago

@erisdiscord Yep, I'm making two different arguments. My first argument is that .endPath applies to an object that is five lines above the statement, which means the reader has to keep more context in his head to understand what should be dead-simple code. My second argument is that the rules of the language wouldn't be obvious to a newcomer.

erisdev commented 12 years ago

@showell I think we have a fundamental disagreement on the issue, so I'm gonna let this be my last comment on the matter. I don't think you're wrong per se, but I don't think you're right either. ;D

It's a potentially very useful bit of syntax sugar that definitely has potential for abuse, but I feel like the potential for convenience outweighs the potential for bad code.

Sorry for repeatedly editing my comments, meanwhile; I just keep noticing things that I left out or could have phrased better. I think I'm happy with what I've written now. C:

showell commented 12 years ago

@erisdiscord asks how these snippets compare in terms of readability:

$ 'article'
  .applySomeStyle()
  .find 'h1'
    .makeNeatHeading()
    .addToTOC()
  .find 'p'
    .extractPullQuotes()

if entity?
  entity.updatePosition()
  unless 0 <= entity.x < width
    entity.x += width
    doSomething()
  unless 0 <= entity.y < height
    entity.y += height
    doSomething()

Vagueness and contrivance aside, the second example is more readable, because every individual statement in the second example explicitly refers to all the objects it's invoking/referencing (e.g. "entity", "width", "height", "doSomething"). I'm not oblivious to the fact that you still need to read up in the code to know whether the statement even executes, but that doesn't undermine my argument.

showell commented 12 years ago

@erisdiscord I think your examples will help clarify the debate. Obviously, it's ultimately a judgment call, as I can't see how there's any clear "right" or "wrong" answer. No prob on the edits, I think my answers still make sense in context.

michaelficarra commented 12 years ago

@showell has made some very convincing arguments. I agree with everything he's said. -1.