Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve chaining syntax #1495

Closed
JulianBirch opened this issue Jul 8, 2011 · 180 comments
Closed

Improve chaining syntax #1495

JulianBirch opened this issue Jul 8, 2011 · 180 comments

Comments

@JulianBirch
Copy link

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
Copy link
Collaborator

I think this is a duplicate of #1251

@JulianBirch
Copy link
Author

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
Copy link
Collaborator

satyr commented Jul 9, 2011

Also see #1407.

@devongovett
Copy link

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
Copy link

erisdev commented Jul 12, 2011

@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
Copy link

@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
Copy link
Author

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

@arbales
Copy link

arbales commented Jul 16, 2011

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

@TrevorBurnham
Copy link
Collaborator

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
Copy link

qrgnote commented Oct 15, 2011

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
Copy link

tcr commented Oct 15, 2011

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
Copy link
Author

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
Copy link

qrgnote commented Oct 25, 2011

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
Copy link

patrys commented Oct 26, 2011

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
Copy link
Author

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
Copy link

patrys commented Oct 26, 2011

@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
Copy link
Author

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
Copy link

$ "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
Copy link

qrgnote commented Oct 30, 2011

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
Copy link

patrys commented Oct 30, 2011

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
Copy link

erisdev commented Oct 30, 2011

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
Copy link

$('#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
Copy link
Owner

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

@raganwald
Copy link
Contributor

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
Copy link

erisdev commented Oct 30, 2011

@raganwald 👍 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
Copy link

qrgnote commented Oct 30, 2011

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
Copy link
Contributor

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
Copy link

tcr commented Oct 30, 2011

@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
Copy link

tcr commented Oct 30, 2011

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
Copy link
Collaborator

satyr commented Oct 30, 2011

@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.

@rev22
Copy link

rev22 commented Jul 1, 2013

I think the clearest solution is chaining method calls on the same indentation level:

    new Thing
    .add foo ...
    .add bar ...

equivalent to:

    (new Thing).add(foo ...).add(bar ...)

@00dani
Copy link

00dani commented Aug 13, 2013

I think Coco's approach of:

firstCall argument, argument
  .method arg
  .otherMethod otherArg

is sufficient for most libraries that use chaining, as well as (I think) quite intuitively looking like what it means:

firstCall(argument, argument).method(arg).otherMethod(otherArg);

@erisdev
Copy link

erisdev commented Aug 13, 2013

@00Davo with the indentation as it is, that looks to me more like

var x = firstCall(argument, argument);
x.method(arg);
x.otherMethod(otherArg);

which is arguably more useful since it works identically for common use cases like jQuery, Q, &c. and the behaviour is even more useful for cases where you might want to call multiple methods on the result of one function call.

@00dani
Copy link

00dani commented Aug 13, 2013

@erisdiscord

I'd personally think unindented chains

firstCall arg, arg
.method arg
.otherMethod arg

would have that meaning, with indented chains having the meaning I suggested.

So perhaps it's not quite as clear as I thought. Hmm.

@erisdev
Copy link

erisdev commented Aug 13, 2013

@00Davo see, to me, if you want meaningful indentation in method chaining, it'd have to look like this, which can get messy with long chains:

firstCall arg, arg
  .method arg
    .otherMethod arg

I think this is part of the reason it hasn't happened yet. (I'm going to bed now; it's 2am)

@00dani
Copy link

00dani commented Aug 13, 2013

Well, making "same-level" indented chains work like Smalltalk cascading actually doesn't mean they'll work right with Q or with jQuery. For instance:

# jQuery
$ 'selector'
  .filter predicate
  .hide()
# Q
readFile path
  .then YAML.parse
  .then processReadYamlObject
  .nodeify callback

Having the chaining syntax compile to equivalently chained methods in JavaScript is, I think, preferable for reasons like those. (Also, libraries like jQuery are explicitly built to be chainable. JavaScript itself doesn't have cascading syntax, so fewer libs are built with that use in mind.)

Hmm. What about actually using ; to indicate a cascade, as in Smalltalk? First-blush try at doing that:

readFile path
  .then YAML.parse
  .then processReadYamlObject
  .nodeify callback
readFile path
  ;then YAML.parse
  ;then processReadYamlObject
  ;nodeify callback

Compiles to:

readFile(path).then(YAML.parse).then(processReadYamlObject).nodeify(callback);
_ref = readFile(path);
_ref.then(YAML.parse);
_ref.then(processReadYamlObject);
_ref.nodeify(callback);

@Soviut
Copy link

Soviut commented Sep 5, 2013

in LESS they use & to indicate continuation of a selector since the dot is already used for selecting css classes, for example:

a.button {
    &:hover { }
}

Compiles to:

a.button:hover {
}

I propose we replace the dot syntax for chained calls with & and consider it a metaphor for "and then, and then, and then".

$ 'a.button'
    &css 'color', 'red'
    &show 'slow'

(It literally would be &then when used with a promise).

readFile path
    &then YAML.parse()
    &then processReadYamlObject()

The only other similar syntax I think makes sense is the ^ which could be a metaphore for "I belong to whatever is above me at this indentation level". In the same case, it would replace the dot at the beginning of the lines.

$ 'a.button'
    ^css 'color', 'red'
    ^show 'slow'

@danschumann
Copy link


PROPOSAL: Periods(following a space) terminate the most recent parenthesis.


Parenthesis, or the lack thereof, are one of the best things about coffeescript.
Parenthesis in any situation inclines the programmer to add code in 2 places, as opposed to 1.

changing
(figure a)

$('.selector').
   .append 'appends 1'

to become
(figure b)

 $('.selector').
   .append('appends 1')
   .append 'appends 2'

In figure b, two lines are identical but look differently. You must replace a space with a (, and then insert at the end.

Proposed syntax:
(figure c)

$('.selector').
   .append 'appends 1' .
   .append 'appends 2'

figure c has both lines looking similarly. Only a single Insertion is needed with no replacements.
Example results: fewer keystrokes producing more symmetrical, readable code


CURRENTLY HOW IT WORKS AND HOW IT COULD WORK( The poor example)

Keep as much the same as possible. Currently, while it is smelly, figure 1 is possible
figure 1:

$('.selector').append $('<div>')
  .append('div contents')
  .click ->
    console.log 'div function'

Keeping figure 1 and adding functionality that would not break old code is essential to not pissing off the user base.

figure 2: (Currently produces syntax errors.)

$ '.selector' ..append $ '<div>' .
  .append 'div contents' .
  .click ->
    console.log 'div function'

Ideally, both compile to
figure 3:

$('.selector').append(
  $('<div>')
    .append('div contents')
    .click(function(){ return console.log('div function'); })
)

@dashed
Copy link

dashed commented Dec 4, 2013

Shouldn't this issue be closed because of 563f14b?

@vendethiel
Copy link
Collaborator

No, this discussion is about cascades.

@xixixao
Copy link
Contributor

xixixao commented Dec 6, 2013

We should decide whether "we got what we wanted".

@vendethiel
Copy link
Collaborator

I'd say yes, for now at least :).

@JulianBirch
Copy link
Author

As OP, I'd say it looks closed.

@blixt
Copy link

blixt commented Sep 20, 2014

The other issues mentioning cascades seem to have been closed due to being a duplicate for this. So I guess this task now has to be considered the canonical request for cascades, rather than just simply chaining as it was originally intended for (or we create a new issue for cascading and don't close it as a duplicate of this).

Here's an example of cascading in Dart: http://news.dartlang.org/2012/02/method-cascades-in-dart-posted-by-gilad.html

I think this would be really helpful because some APIs simply don't allow for the existing chaining, Promise being the most obvious one:

myPromise
  .then ->
    someOtherPromiseOp()
  .catch ->
    console.error 'oh no, this error might have happened in someOtherPromiseOp, not in myPromise!'

This would solve the above:

myPromise
  ..then ->
    someOtherPromiseOp()
  ..catch ->
    console.error 'we know that myPromise threw, not someOtherPromiseOp'

@usergenic
Copy link

Nested cascades in CoffeeScript would be a snap with the .. syntax, but I could see a single . prefix syntax could work along with the indent to promote the same idea.

document.queryAll "#mypanel TABLE.firstCol"
  .classes.remove "firstCol"
    .style
      console.log("current background is: ", .background)
      .background = "red"
      .border = "2px solid black"
  .nodes.add(new Element.html "<spam>This cell is now red</span>")

@1j01
Copy link

1j01 commented Jan 5, 2016

@brendan .classes.remove(...).style? Also, new Element.html means new (Element.html)
I guess you're used to dart with .classes.remove etc.

document.querySelector "#mypanel table tr"
  .classList.remove "first-row"
  .style
    console.log "current background is: ", .background
    .background = "red"
    .border = "2px solid black"
  .appendChild new Element
    .innerHTML = "<span>This cell is now red</span>"

I'm not sure how I feel about this. The . after console.log is ambiguous unless you will no longer be able to do . on a new line without indenting as you can now.

At any rate, I can't disagree with notion that "writing functions to return a certain thing just to cater to how you like to write programs is hacking around a missing language feature"

@usergenic
Copy link

Didn't realize that CoffeeScript would interpret

x
  .y
  .z

as x.y.z.

I guess I'd amend the example you posted to be:

document.querySelector "#mypanel table tr"
  ..classList.remove "first-row"  # querySelector result's classList
  ..style # querySelector result's style
    console.log "current background is: ", ..background # style's background
    ..background = "red" # style's background
    ..border = "2px solid black" # style's border
  ..appendChild new Element # querySelector result's appendChild
    ..innerHTML = "<span>This cell is now red</span>" # appendChild result's innerHTML

@stugol
Copy link

stugol commented Mar 14, 2016

Is this likely to be implemented soon?

@GeoffreyBooth
Copy link
Collaborator

I’m not sure what is even still being debated on this thread. If someone wants to improve chaining from where it stands in v2, please open a new issue with a specific proposal.

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

No branches or pull requests