0

I've been using CoffeeScript for a while. I find it a good language overall, certainly better than plain JS, but I find I'm still baffled by its indentation rules. Take this example:

Bacon.mergeAll(
    @searchButton.asEventStream('click')
    @searchInput.asEventStream('keyup')
        .filter (e) => e.keyCode is 13
)
.map =>
    @searchInput.val()
.flatMapLatest (query) =>
    Bacon.fromPromise $.ajax
        url: @searchURL + encodeURI query
        dataType: 'jsonp'

This does what it should (the code is based on this tutorial, btw) but it took me a lot of trial and error to get it right.

Why do mergeAll and asEventStream require parentheses around their arguments? Why is indentation not enough to determine where their argument lists begin and end? OTOH, why is indentation enough for map and flatMapLatest? Why is the whitespace before a hanging method, such as .filter (its indentation level) not enough to determine what it binds to? It seems to be completely ignored.

Is there a definitive guide to this language's indentation rules? I never had a problem understanding Python syntax at a glance, even with very complex nesting, so it's not an issue with indentation-based syntax per se.

Tobia
  • 17,856
  • 6
  • 74
  • 93
  • Just because you can use anonymous functions doesn't mean you have to. If you bust your functions out and give them names then you can convert a confusing mess of mental parsing and guesswork with simple and readable things like `o.map(mangle_them).filter(out_the_garbage)...` – mu is too short Dec 08 '13 at 19:09
  • Yes, I'd surely do that in production code. I'm just making sure I understand all the nuances of the syntax, lest I fall into some trap. – Tobia Dec 09 '13 at 10:16

2 Answers2

1

Indentation in CoffeeScript generally defines blocks, and argument lists aren't (necessarily) blocks. Similarly, a chained function call isn't a block; CoffeeScript simply sees a line starting with . and connects it to the previous line of similar or lower indentation.

Hence, the parentheses are needed for asEventStream, since CoffeeScript would otherwise see:

 @searchInput.asEventStream 'keyup'.filter (e) => e.keyCode is 13

Which would call filter on the 'keyup' string, and it'd remain ambiguous whether the function is an argument to filter, or an argument to @searchInput.asEventStream('keyup'.filter)(). That last bit obviously doesn't make much sense, but CoffeeScript isn't a static analyzer, so it doesn't know that.

A function, meanwhile, is a block, hence the function argument to .map() works without parentheses, since it clearly delimited by its indentation. I.e. the line following the function has less indentation.

Personally, I'd probably write

Bacon.mergeAll(
  @searchButton.asEventStream('click'), # explicit comma
  @searchInput.asEventStream('keyup').filter (e) -> e.keyCode is 13 # no need for =>
)
.map(=> @searchInput.val()) # maybe not as pretty, but clearer
.flatMapLatest (query) =>
  Bacon.fromPromise $.ajax
    url: @searchURL + encodeURI query
    dataType: 'jsonp'

In fact, I might break it up into separate expressions to make it clearer still. Insisting on the syntactic sugar while chaining stuff can indeed get confusing in CoffeeScript, but remember that you're not obliged to use it. Same as you're not obliged to always avoid parentheses; if they make things clearer, by all means use 'em!

If the code's easier to write, less ambiguous to read, and simpler to maintain without complex chaining/syntax (all of which seems true for this example), then I'd say just skip it.

In the end, there just are combinations of indentation syntax in CoffeeScript that can make either you or the compiler trip. Mostly, though, if you look at something, and find it straightforward, the compiler probably thinks so too. If you're in doubt, the compiler might be too, or it'll interpret it in unexpected ways. That's the best I can offer in terms of "definitive guide" (don't know of a written one).

Flambino
  • 18,507
  • 2
  • 39
  • 58
  • Thank you, it makes sense. CS does indeed connect a line starting with a dot "to the previous line of similar or lower indentation", except that not everything is considered a block. `if`, `while`, and function definition are, while a parameter list isn't. – Tobia Dec 09 '13 at 10:20
  • Regarding the use of fat arrow `=>` I've posted a rant here, in case you're interested: http://stackoverflow.com/questions/13682986/when-building-classes-in-coffeescript-is-there-ever-a-reason-to-not-use-the-fat/17431824#17431824 – Tobia Dec 09 '13 at 11:12
  • @Tobia Heh, good rant. Personally, with the code I often write, the fat arrow a "special case". It's not necessary often enough to justify _always_ using it. I.e. I usually _don't_ need a bound function, so when I do, I like to be explicit about it. That said, your arguments are not without merit; if my projects were different, I'd consider it. – Flambino Dec 09 '13 at 11:47
1

Have you looked at the Javascript produced by this code? What happens when you omit ().

In Try Coffeescript I find that:

 @searchButton.asEventStream 'click'

is ok. The second asEventStream compiles to:

this.searchInput.asEventStream('keyup').filter(function(e) {

but omitting the () changes it to:

this.searchInput.asEventStream('keyup'.filter(function(e) {

filter is now an attribute of 'keyup'. Putting a space to separate asEventStream and ('keyup') does the same thing.

@searchInput.asEventStream ('keyup')

As written .mergeAll() produces:

Bacon.mergeAll(...).map(...).flatMapLatest(...);

Omitting the ()

Bacon.mergeAll
    @searchButton.asEventStream('click') 
    @searchInput.asEventStream('keyup')

gives an error because the compiler has no way of knowing that mergeAll is a function that takes arguments. It has no reason to expect an indented block.

Coming from Python, my inclination is to continue to use (),[],{} to mark structures like arguments, arrays and objects, unless the code is clearer without them. Often they help me read the code, even if the compiler does not need them. Coffeescript is also like Python in the use of indentation to denote code blocks (as opposed to the {} used in Javascript and other C styled languages).

hpaulj
  • 221,503
  • 14
  • 230
  • 353