Closed JulianBirch closed 7 years ago
I think this is a duplicate of #1251
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")
Also see #1407.
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.
@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'
@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.
Whilst my preference for as little punctuation as possible is marked, I can't see anything unambiguous with Devon's rule.
+1 for @ devongovett's proposal — this might be the behavior I'd expect anyway.
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.
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"
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.
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, ""))) {
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
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:
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.
@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.
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.
$ "p.neat"
.addClass "ohmy"
.show "slow"
this is exactly what I'm looking and expecting for. parenthesis is really a pain when doing chaining
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.
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 :)
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)
$('#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 :)
Food for thought from @raganwald: http://news.ycombinator.com/item?id=3175028
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."
@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.
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
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()
@raganwald +1. This seems like an intuitive, semantic distinction between chaining vs continuation of previous lines.
Moved comments about chaining parentheses omission to #1407.
Just to recap, it looks like we're discussing three problems:
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')
Permitting multi-line paren-free chaining syntax:
$ 'body'
.html '<h1>hey</h1>'
.addClass 'cool'
require 'foo' .. baz
@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.
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?
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();
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.
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. :)
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');
What about:
with $ 'div'
with @find 'span'
@addClass 'span-element'
Not that it would make the code any shorter than JS.
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.
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...
{bar} = require 'foo'
is the same as
bar = require('foo').bar
@JulianBirch cool didn't realize that, thanks. :)
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.
@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.
@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.
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.
@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.
@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.
@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.
@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:
@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.
@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.
@showell has made some very convincing arguments. I agree with everything he's said. -1.
Since chaining is a pretty common feature of Javascript libraries, how about a dedicated syntax for it?
e.g.
could be written
Alternatively, you could go with something insanely LISPy
NB, the semantics I'm proposing would be equivalent to