7 Rules To Write Better CoffeeScript Code Without Pain
I often hear that CoffeeScript isn't that great, that not having parenthesis
is not good for readability, and so on. I think that it mostly comes from the
way people write CoffeeScript, rather than the language itself.
CoffeeScript allows you to write easy to read code in a fast way. It's close
to pseudo-code, it strips everything unecessary and it's very good at
highlighting wrong code designs. Specially, because it's based on indentations
(by the way I recommend four spaces long indentation).
This article will give you hints on usual mistakes and will point you out some
generic programing pitfalls, like nesting too much code.
1. There is no absolute rule
This is actually the only absolute rule. Which proves my point :)
Ruleception! It's not because you can read "no parenthesis in CoffeeScript"
that you mustn't use them when necessary. In the same way, it isn't because
it's possible that you should do it at all cost. Don't forget they are
guidelines, not The Truth.
2. Keep it small
In introduction I said CoffeeScript is based on indentation. Thus, having such
a limit is very good at showing where you have too much nested code, thus
helping you improving your code. This is a completely generic programming
advice though!
Define a charaters per line limit
We use a 80 characters limit at Cozy and I'm very happy with it. I'd like to
add that I've seen an other common recommendations at 120 characters, but 80
has proven itself to be just perfect again and again for me. People saying it's
too little probably write too nested code ;-)
A good idea to rember that limit is to set a vertical ruler in your favorite
IDE (I use SublimeText but almost all of them have the feature).
Write short functions
The end of big functions are hard to track because CoffeeScript is
indentation-based, and big functions tend to be complex with many conditional
structures. Eventually, asynchronous calls make it even worse. The deep issue
is always nested code, big functions are just a symptom. So here is my advice:
use the symptom to cure the disease!
3. Avoid parenthesis
One of the thing I love with CoffeeScript is that most parenthesis are not
mandatory. In fact, most parenthesis can be removed.
Let's answer the question: when should you use parenthesis?
- In conditional statements (if, loops), always use parenthesis to prevent
wrong interpretation. Always putting parenthesis avoids the risk of any
possible buggy statement.
{% highlight coffeescript %}
valid
if 'something' is myObj.myFunc 'some-parameter'
invalid
if myObj.myFunc 'some-parameter' is 'something'
because it's an equivalent of
if myObj.myFunc('some-parameter' is 'something')
{% endhighlight %}
- When you chain function calls
{% highlight coffeescript %}
valid, still readable
myVar = myFunc1 myObj.get 'my-property'
valid, less readable
myVar = $.myFunc1 anObj.myFunc2 myObj.get 'my-property'
valid, unreadable
myVar = $.myFunc1 anObj.myFunc2 $.myFunc3 _.myFunc4 myObj.get 'my-property'
valid, and readable
myVar = $.myFunc myObj.get('my-property')
valid, and readable
myVar = $.myFunc1 myObj.myFunc2(myObj.get('my-property'))
valid, and unreadable
myVar = $.myFunc1 myObj.myFunc2($.myFunc3(_.myFunc4(myObj.get('my-property'))))
{% endhighlight %}
You can try more examples with parameters for the various functions,
parenthesis versions are almost easier to read.
But where do you set the limit? How many functions in the chain before using
parenthesis? Chose one and stick to it!
I find relevant to set the limit to 1 for two reasons. First, if you have to
many chain calls, something might be wrong. Second, it might be easier to
understand for people that are not used to read CoffeeScript.
4. Explicit returns
The last line of every piece of code is turned into a return
call. Please, don't do implicit return, most of the time it makes things
confusing.
{% highlight coffeescript %}
valid, but not readable
myFunc = ->
myString = substring 'example', 1
something = myString + anotherString
something
valid, and readable
myFunc = ->
myString = substring 'example', 1
something = myString + anotherString
return something
{% endhighlight %}
As no rule is absolute, there is some case where implicit return is handy:
{% highlight coffeescript %}
valid, readable, and concise
myMappedCollection = myCollection.map (object) -> object.property
{% endhighlight %}
I won't blame anyone willing to always use an explicit return, though.
Explicit returns also allow you to track the end of function more easily if you
haven't managed to make it small enough. It's all good!
5. Control your flow
The advice I'm going to give are highly controversial. We do not agree on the
right way of doing things within the team. So I'm going to expose you the
problem and suggest some solutions.
I've been talking how bad nested code is, especially in CoffeeScript (it's even
more important because know you have a 80 characters per line limit!). In
JavaScript, we tend to write a lot of asynchronous stuff which makes it even
worse. Among the use cases, XHR (Ajax requests) is an obvious one. If you do
NodeJS you will find plethora of examples.
Putting that straight, asynchronous calls
are generate a lot of asynchronous call. They will turn your code into a mess if
you're not careful. Flow control in JavaScript deserves an article on its own
so let me just introduce you to the issue.
You want to retrieve multiple informations from the server to say, pre-fill a
form, but that could be anything really.
{% highlight coffeescript %}
getUserInformation (err, user) ->
getCountryInformation (err, country) ->
getOrders (err, orders) ->
getRelatedProducts (err, relatedProducts) ->
# do something with all those data
{% endhighlight %}
Good, now let's handle the error case
{% highlight coffeescript %}
$.get '/user/:id', (err, user) ->
if err?
# do something with err
else
$.get '/country/:id', (err, country) ->
if err?
# do something with err
else
$.get '/orders/:userId', (err, orders) ->
if err?
# do something with err
else
$.get '/relatedProducts/:userId', (err, products) ->
if err?
# do something with err
else
# do something with all those data
{% endhighlight %}
You could argue that we can handle the error case with one liner, but I just to
make a point: nesting functions with conditional structures lead to the
callback hell.
To solve this, you can use subfunctions, but that doesn't scale well because you
end not knowing if you are in your main function or in a subfunction, and
having to navigate between your main function body and the subfunctions to know
what is going on.
You can also use Promises, but people disagree on where Promises should be used
and where they shouldn't.
What I want to suggest you is using Async, a
popular library. Async has the big drawback of being very big but if you find
yourself falling into this again and again in your code, it might worth it.
{% highlight coffeescript %}
same code, with async
async = require 'async'
all requests will be run in parallel
async.parallel [
(cb) -> $.get '/user/:id', cb
(cb) -> $.get '/country/:id', cb
(cb) -> $.get '/orders/:userId', cb
(cb) -> $.get '/relatedProducts/:userId', cb
], (err, results) ->
if err?
# deal with the error
else
[user, country, orders, relatedProducts] = results
# do something with the data
{% endhighlight %}
6. It's small but it matters
The last rule is actually a bunch of them. There are small details that are
nice improvements to the way you write CoffeeScript.
- Write operators in english, e.g. use
is
instead of==
andand
for&&
. - Don't use
{}
to define objects unless it's mandatory. - To assign value to variable only if it's undefined use
?=
. - If your array or object is defined on several lines don't use commas.
- Do you see something else? Let us know!
7. Use a linter
This true whatever language you use, but lint your code. An automatic checking
of all your code style rules you define is very good to have. We use
CoffeeLint, especially to track characters per
line limit and indentation issues, but there are many more options that could
fit your use.
Don't forget that the big deal is coherence in your code, once you have rules,
write them down somewhere for your
whole team and stick to them.
So What did you think of this article? What guidelines do you suggest? Let's
share them on the
forum!