Low-precedence method call syntax for paren-free chaining #2114

Closed
avdi opened this Issue Feb 10, 2012 · 38 comments

Comments

Projects
None yet

avdi commented Feb 10, 2012

Inspired by this discussion, where @jashkenas suggested I file a ticket for it.

Synopsis

Before:

$('some_selector').click (e) -> 
  #... handler code...

After:

$ 'some_selector' .. click (e) -> 
  #... handler code...

Details

Add a new syntax for calling methods with low precedence, suitable for chaining. In the example above I use .., but that's just a suggestion off the top of my head; it could be anything.

Why?

As I'm finally getting a chance to use CoffeeScript for real, I've been immediately struck by two things: the language and conventions strive to avoid extra parenthesis (yay!) but in many common cases that's not completely possible (boo!). As a result, I see a lot of code like the "before" example which mixes paren and paren-free calling. Since I strive for code in which "same things look the same", this was a bit jarring. When I first saw the code above it signaled to me that there was something "special" about the call to jQuery's $() method, since it required parens and the call to .click did not. As it turns out the only thing special about is was that it was not at the end of the line.

The suggested change would enable even more put-the-top-down-and-let-the-breeze-blow-through-your-hair paren-free coding, which I think would be a Good Thing.

cfcosta commented Feb 10, 2012

👍

Elland commented Feb 10, 2012

👍

👍

To me, the "before" actually looks a lot better and is more readable. The argument of "making the same things look the same" breaks down because now there are two different syntaxes for calling a function . and .., so this won't actually solve the problem.

👍

showell commented Feb 10, 2012

-1

This feature adds complexity to the language, and it's obviously not essential. The ".." syntax feels very cryptic to me, and the only alternative I can think of is, um, parentheses.

👎, chainable APIs sucks. https://gist.github.com/1730755

Owner

jashkenas commented Feb 10, 2012

Hey folks -- although I've said this before in other tickets ... simply 👍-ing a post doesn't help get features added to CoffeeScript, because the sad reality of Github issues is that they aren't a democracy.

If you think this feature is a good idea, tell us why. If you think it could be improved, tell us how. Or if you think that @avdi's proposal is 100% complete and perfect as it stands, tell us that. ;)

Elland commented Feb 10, 2012

good point, @jashkenas. I like the idea of avoiding all parens for methods and I would expect a method chain to be evaluated from left to right just as in Ruby, Obj-C etc., I think the two-dots is a bit cryptic as well, and it looks a bit too much as instance..class_method in Ruby to make me comfortable with it. I think "simple" left-to-right evaluation of a method chain by default could solve the issue.

yuchi commented Feb 10, 2012

I like this proposal a lot more than of the over-engineered proposes of #1495

Correct me if I'm wrong, can this operator can be described as:

.. operator closes every implicit parens opened from the start of the expression.

Cannot wait to be able to do $ '.sel' .. on 'click', '.els', (e) -> false

Elland commented Feb 10, 2012

@yuchi it certainly is

The first time I wrote a jQuery selector in CoffeeScript I tried to write:

$ "a:first-child".click (event) ->

While it's hard to tell what @avdi's suggested syntax could cause in the long run, it has one clear disadvantage: not being intuitive at all.

Won't we still need parenthesis for function calls that don't have arguments? If this proposal wen't through, someone might expect this to work:

$ '#selector' .. hide ..

Turning into this:

$('#selector').hide()

avdi commented Feb 10, 2012

@rubymaverick: that's another very surprising thing about CoffeeScript syntax, and one which I hope will be addressed some day. But that's a discussion for another ticket; and one which I rather imagine you've all talked to death by now :-)

avdi commented Feb 10, 2012

@yuchi: hah, the .. was completely off the top of my head for the sake of an example; funny that someone else used it as well.

Coco: $ '#select' .click -> do stuff => $('#select').click(function(){ stuff(); });

if someone were ever to mean '#select'.click by this, they should always have left the space out
the space is also pretty clear, actually...

e; also, no-one?? will expect $ '#select' .click . to work, the last dot doesn't look like it'll compile to ()

zenhob commented Feb 10, 2012

There are some things I like about this idea, but there was something in your justification that struck me as out of place:

When I first saw the code above it signaled to me that there was something "special" about the call to jQuery's $() method, since it required parens and the call to .click did not. As it turns out the only thing special about is was that it was not at the end of the line.

Wouldn't the two dots (or whatever) still signal that there was something "special" about that call? It seems like there is the potential to replace one possible confusion with a greater one, since we are now introducing another method call syntax.

avdi commented Feb 10, 2012

@zenhob: The difference I am concerned with in this particular case is false differences signaled within a single line of code. To better illustrate, here's a more egregious example:

name = $('.foo').closest('form').find(':file').attr 'name'

In that example, .attr 'name gets to be all fancy and paren-free simply because it's on the end of the line. But there's actually no semantic difference from any of the other query methods chained before it.

Compare:

name = $ '.foo' .. closest 'form' .. find ':file' .. attr 'name'

In this example there is exact syntactical parity for each call in the chain (and fewer parens FWIW). Yes, this may differ from other lines in which the single . is used. For me, this is less problematic than syntax switching in a given line, but YMMV.

omghax commented Feb 10, 2012

What about using : for function invocation, a la Smalltalk? I think this looks nice and avoids unnecessary parentheses:

$: "#some_selector" click: (e) ->

The only problem is the ambiguity with object literals, so this wouldn't work:

$: "#some_selector" css: display: "block", "font-style": "italic"

because it'd be impossible to tell whether that should compile to css({display: "block"}) or css(display("block")). In that case you'd need to wrap the literal in curly braces:

$: "#some_selector" css: {display: "block", "font-style": "italic"}

zenhob commented Feb 11, 2012

Counterpoint:

$('.foo').closest('form').find(':file').remove()

Compare:

$ '.foo' .. closest 'form' .. find ':file' .. remove()

The more I look at this the less I think it's needed.

mcmire commented Feb 11, 2012

This issue has actually come up before in #1407 (also #1495, at least the first part), and discussion floated around the same sort of proposal:

$ '.foo' > closest 'table' > find 'tr' > each (i, tr) ->
$ '.foo' .. closest 'table' .. find 'tr' .. each (i, tr) ->
$ '.foo' ... closest 'table' ... find 'tr' ... each (i, tr) ->

This syntax also came up:

$ '.foo' .closest 'table' .find 'tr' .each (i, tr) ->

with the possible surprise that these would not be equivalent:

get words .split ' ' .filter (w) -> r.test w .join(', ')
get words .split ' ' .filter((w) -> r.test w).join(', ')

There are obviously pros and cons to both approaches. I'm not a fan of adding an operator to CoffeeScript that doesn't have an equivalent in JavaScript, although it could be argued that CoffeeScript certainly has syntax constructs that are not valid JavaScript, so what would be the harm of adding a operator that is not valid JavaScript either. I'm also not a fan of the "missing parentheses" approach because it seems too magic to me, although again it could be argued that CoffeeScript already lets you omit parentheses, so filling them in for you when you chain methods isn't that far of a stretch (there is also a multi-line chaining syntax in this case to consider, although that is another topic).

That said, I think @avdi's argument is a good one. It's not that there are two different ways to say the same thing in CoffeeScript -- it's that it's possible to write code that looks like it does something different when compared with a differently written piece of code, when in fact it doesn't. I totally get this -- I really dislike languages where how they look doesn't match how they work. So just for that, I'm sold on fixing this issue. As to which solution to go with exactly, as I mentioned I would prefer

$ '.foo' .closest 'table' .find 'tr' .each (i, tr) ->

because it seems like could be easily consistent with multi-line chaining, but that is just me.

showell commented Feb 11, 2012

I'm sure this proposal is dead in the water. Algol got a lot of things wrong, but not parentheses. Parentheses are actually a great invention. I've never seen such ugly code in my life until this issue. Grossssssssssssssssss.

Collaborator

satyr commented Feb 11, 2012

How would you paren-free these with this proposal?

f(x)?.p
g(y)[q]

mcmire commented Feb 11, 2012

Hmm. If you wanted to paren-free either of those, f x ?.p makes sense for the first case (f x(?.p) is invalid so it shouldn't get interpreted that way). g(y)[q] could be written as g y [q], but that's ambiguous.

Interesting... so here's a full-on experiment:

a(b).c(d)[e](f)?.g.h((a,b) -> c)

Going completely paren-free, it's admittedly kind of a Magic Eye experience:

a b .c d [e] f ?.g.h (a,b) -> c

And the other approach:

a b .. c d .. [e] f ?.. g .. h (a,b) -> c

Opinion: the c d .. [e] f bit is foreign, but if you replaced it with, say, x, then it would be c d .. x f, or c(d).x(f) and thus you could make the mental jump to c(d)[e](f).

Not saying I'd use it, but it's interesting, anyway.

Thoughts, @avdi?

Parens can be gross but in this case of chaining methods together it feels "right" and using .. seems cryptic. If anything you would want to use an ampersand to chain since you are literally say "do this and this and this".

$ "#foo" & addClass "bar" & removeClass "baz"

or if you fancy a C++ approach

$ "#foo"& addClass "bar"& removeClass "baz"

It would introduce another non-word character back into the mix but I think it makes more since than ..

erisdev commented Feb 11, 2012

@chadhietala That proposal is even worse IMO because & is already a thing and it compiles like this:

$("#foo" & addClass("bar" & removeClass("baz")));

Ah @erisdiscord yes you are correct. Forgot & is a bitwise "AND" in JS. hmmm :-\

Contributor

davidchambers commented Feb 12, 2012

I understand where you're coming from, @avdi. I obsess over this myself. I've found piece of mind by more or less always using parens when invoking jQuery methods.

If consistency is what you're after, the solution is…

name = $('.foo').closest('form').find(':file').attr('name')

not…

name = $ '.foo' .. closest 'form' .. find ':file' .. attr 'name'

To further @paulmillr's point, it's worth noting that method chaining is not necessarily a practice we should encourage. In a Hacker News thread, @raganwald commented:

My suggestion is that "chaining" method calls is a syntax issue and not a function issue, and 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.

jrus commented Feb 12, 2012

Attribute access via .s is only one type of access in JavaScript, and if there is a way to close implicit parens by writing foo bar .. baz for foo(bar).baz, it would also be useful to make it usable for bracket access, as in foo bar » [baz] for foo(bar)[baz], or for calls, as in foo bar » baz for foo(bar)(baz) (where the » symbol is used as a placeholder here).

(Edit: looks like mcmire said some of this)

In general though, I think this leads to too much ambiguity. Implicit parens can be arbitrarily nested in CoffeeScript, across multiple levels of indentation, across function definitions, etc., and I’d guess it’s often hard to figure out just how many levels of them were supposed to be closed. Some examples:

Does foo = bar .. baz(foo = bar).baz or foo = bar.baz?

Does foo bar = baz .. quxfoo(bar = baz).qux or foo(bar = baz.qux)?

Does foo bar -> baz .. quxfoo(bar(function(){return baz})).qux or foo(bar(function(){return baz.qux}))?

What happens if we add some indentation?

foo bar ->
    baz .. qux

or maybe

foo bar ->
    baz
  .. qux

etc.

avdi commented Feb 12, 2012

@davidchambers:

If consistency is what you're after...

Unfortunately that solution doesn't address the original example, in which using consistent parenthesis means losing the advantages of significant whitespace to delimit a code block.

method chaining is not necessarily a practice we should encourage

There are two distinct kinds of chaining. Writing methods for K-combinator-style chaining, in which the method is called for its side-effect, the result is ignored, and the original receiver returned, is indeed a prop for a language deficiency. Smalltalk, notoriously stingy with its syntax, dedicates an operator to this idiom (called "cascading" in Smalltalk).

rectangle
  height: 71;
  width: 42

On the other hand you have functional chaining, where you successively call methods on the result values of preceding methods in order to progressively transform the results. For instance, in Ruby we might generate a "slug" with:

title.strip.downcase.tr_s('^[a-z0-9]', '-')

I don't think there's any way around this type of chaining in an OO language, and I'm not sure there's any reason to want to avoid it.

I think it may be especially easy to confuse these two types of chaining in JavaScript, since we're all familiar with jQuery, which freely intermixes the two styles. E.g. .hide() is imperative, and only returns the original receiver as a convenience. .filter() on the other hand is functional, and the only reason to call it is to apply some other query, or command, to the result.

My second example:

$('.foo').closest('form').find(':file').attr('name')

Is purely functional, and I'm not sure how avoiding chaining would improve it in any way. I mean, we could reformat it to use a Lisp functional composition style:

(attr (find (closest ($ '.foo') 'form') ':file') 'name')

But I'm not sure that's an improvement. In fact, having the composition go the other way 'round is one of the little pleasentries of OO programming, since many of us have an easier time reading the code when the order of transformations goes from left to right.In fact, several modern functional languages provide the ability to switch the visual order of function transformation to something closer to the CoffeeScript version for just that reason.

TL;DR: there are two kinds of chaining; only one of them is indicative of a missing language feature.

Collaborator

michaelficarra commented Feb 12, 2012

@avdi: You're forgetting that you could also write it in a left-to-right way like this:

((($ '.foo').closest 'form').find ':file').attr 'name'
Contributor

davidchambers commented Feb 12, 2012

Great comment, @avdi. Before reading it I didn't really differentiate the two kinds of jQuery method. I agree that chaining is very natural for .filter-like methods.

mcmire commented Feb 13, 2012

@jrus I was going to put a bunch of examples here, but suffice it to say that I think ".." should actually take pretty high precedence -- the same as ".", actually. In other words, I don't think it should "jump" out of functions, assignment expressions, etc. A low-precedence operator doesn't make sense to me... that's just my 2 cents.

Contributor

geraldalewis commented Feb 16, 2012

I'm pretty sensitive to parens, but they're a sometimes necessary aesthetic blemish.

The proposed:

$ 'some_selector' .. click (e) ->

may be no problem for CoffeeScript, but I don't parse it as fluently as $('some_selector').click (e) ->. I'm aware of the fact that it usually takes me a while to read new syntax naturally, but I don't think that's so much at play here.

I think CoffeeScript strikes a good balance between reducing syntax noise and enhancing readability. Semicolons are superfluous because we find line breaks to be natural delimiters. It's logical that CoffeeScript eliminates them since they serve little purpose (for us). However, the terse/opaque nature of programming languages and APIs works against us for mentally delimiting subexpressions. This proposal would be (mentally) really easy to lex and really hard to parse ;)

To be honest, I don't see the need for a new "syntax" for it as such, just a kind of detection to see if the next value after a space would work as a stand-alone value. For example...

a "b" .c // a("b").c
a "b" [c] // a("b")[c]
a b "c" // a(b(c))
a "b" // a("b")
a "b" ("c") // a("b")("c")

Also, it's a little off topic, but the way it detects arguments could be changed, so that the following is true

a "b" "c" // a("b", "c") - currently creates an error
a b c // a(b(c))
Collaborator

TrevorBurnham commented Feb 21, 2012

@mcmire mentioned it, but it's worth mentioning again: This is essentially the same as #1407. That proposal (which I like) is simply to make implicit parentheses end before a . accessor.

$ document .ready -> ...  # $(document).ready(function() { ... })

No need for .. or a new symbol.

@TrevorBurnham That's basically what I was trying to say.

Owner

jashkenas commented Feb 26, 2012

Closing in favor of #1407, in conjunction with @raganwald's proposal for indentation-based chaining and closing of open calls ... both of which accomplish the same goal more elegantly than a new keyword.

@jashkenas jashkenas closed this Feb 26, 2012

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment