Over the last couple of months, I've coded nearly all my client-side code in CoffeeScript. I prefer CoffeeScript to JavaScript ... but there are a few things to watch out for. I thought I'd share some practical experience, rather than the more typical hype for CoffeeScript or the reactionary vibe against it.
My biggest problems with CoffeeScript has been with indentation, which is no surprise, given the basic value proposition of the language. There are places where I've accidentally deleted a single space and completely changed the meaning of my code.
Here's an adapted example. We'll start with the correct CoffeeScript source code, and JavaScript translation:
Now, notice what happens when we delete a single space.
The change in indentation confused the CoffeeScript compiler; it ended the call to View.extend()
and created a new expression for an object literal that is created and thrown away.
And remember, this isn't hypothetical; I really did this, and started seeing very odd errors: undefined properties and events not getting triggered. It took me a bit of time, with the debugger, and adding console logging, and eventually some viewing of the generated JavaScript, to realize what had happened.
Part of my solution to this class of issue, in Emacs, is to automatically increase the font size when editing CoffeeScript files. I may have to consider a switch from spaces to tabs as well, something that I've never considered in the past. I've also noticed that some IDEs, including IntelliJ, draw a vertical line to connect code at the same indentation layer. I don't have enough Emacs hacking experience to accomplish that, but I hope someone does.
Another problem I've hit is with function invocation; CoffeeScript style is to omit the parenthesis following the function name, and just immediately start listing function parameters:
element.setAttribute "width", "100%"
However, if your API allows chaining, you may be tempted to:
element.setAttribute "width", "100%".setAttribute "color", "blue"
Alas, the .setAttribute
binds to the immediate value, the string:
element.setAttribute("width", "100%".setAttribute("color", "blue"));
One option is to enclose the first call with parenthesis:
(element.setAttribute "width", "100%").setAttribute "color", "blue"
I want to like that option, as it stays consistent with the normal format for function invocation; it just captures the result for chaining. However, each additional setAttribute()
call will require an additional nesting layer of parenthesis. This leaves the following as the solution:
element.setAttribute("width", "100%").setAttribute("color", "blue")
And now we are stuck having to parse two different syntaxes for function invocation, depending on some very specific context.
Another issue is that, once you start using the standard function invocation style, which doesn't use parenthesis, you can easily get tripped up:
Here the is null
bound tightly to the "div"
string literal; the result (true or false) was passed to the find()
method, rather than the expected "div"
.
Because of these subtleties, I seem to find myself using the live CoffeeScript to JavaScript preview to check that my guess at how the JavaScript will be generated is correct. Preview is available on the CoffeeScript home page, and also as the CoffeeConsole plugin to Chrome.
What's interesting is that the above concerns aside, most of the time my first pass at the CoffeeScript does produce exactly what I'd expect, making me feel silly to waste the time to check it! Other times, I find that my first pass is actually too verbose, and can be simplified. Here is an example from some code I've written for Tapestry, a simple DSL for constructing DOM elements on the fly:
This is invoking the builder()
function, passing a string to define the element type and CSS class, then an object that adds attributes to the element, and lastly some additional values that define the body of the element: an array to define a nested element, and a string that will become literal text. This is converted to JavaScript:
Turns out, we can get rid of the curly braces on the objects without changing the output JavaScript; CoffeeScript is able to figure it all out by context:
Even getting rid of the commas is allowed. The commas are equivalent to the line break and indentation that's already present:
And that's where I start really liking CoffeeScript, because quite often, the code you write looks very close to a bespoke DSL.
Then there's the debugging issue; CoffeeScript scores poorly here. First of all, the code has been translated to JavaScript; line numbers at execution time will no longer line up with source CoffeeScript at all, so the first and most useful bit of information in a stack trace is no longer useful. It's very easy to glance back and forth to match the generated JavaScript back to source CoffeeScript, but it still takes some effort.
Of course, in a production application, you'll likely have minimized your JavaScript, which mangles your JavaScript and your line numbers far more than the CoffeeScript compilation does!
The other weakness is that all functions in CoffeeScript are anonymous; this takes another useful bit of information out of stack traces: the local function name. This leaves the only way to map from compiled JavaScript back to source CoffeeScript as involving a lot of processing between your ears, and that's not exactly ideal, given everything else you have to mentally juggle.
Lastly, I've heard of people who think CoffeeScript is only for programming using objects and classes. I find this quite odd; it's nice to have some optimized syntax for classes in CoffeeScript even if it comes at some price (the generated JavaScript can become quite verbose adding runtime support for the syntax that allows super class methods and constructors to be invoked); however, far beyond 95% of the code I write is purely functional, with what state that's present captured in closures of local variables, they way it should be.
I like Scott Davis' summary: CoffeeScript will make you a better JavaScript programmer. It is, largely, JavaScript with better syntax, plus a number of new features. You should never think of CoffeeScript as an alternative to understanding the occasionally ugly nuances of JavaScript; instead I think of it as a "have your cake and eat it too" situation. I certainly don't consider it noobscript, the way many in the Node.js community seem to.
At the end of the day, the translation from CoffeeScript to JavaScript is usually quite predictable, and turns into the JavaScript you probably should write, but often would not: for instance, JavaScript that always evaluates inside a hygienic function, or where every variable is defined at the top of its containing block.
My final take is that you are trading the very rough edges present in the JavaScript language for some sensible tradeoffs, and the occasional hidden "gotcha". I think that's a completely reasonable trade, and will continue to write as much as I can, client-side and server-side, in CoffeeScript.
You can actually extend backbone object using coffeescript inheritance:
ReplyDeleteclass ElementView extends View
Very useful!
ReplyDeleteOne other "gotcha" that I may update the main document with: the implicit returns. Generally a good idea, except sometimes your function returns nothing; if you omit an explicit "return" at the end, you'll often see CoffeeScript generating code to accumulate intermediate values, at some expense, and return them ... for no reason. I almost thing CoffeeScript needs an explicit way to describe a function that returns no value ... I guess it just has "return" at the end, but that looks out of place.
ReplyDeleteAgain, implicit return is often a good thing, especially for "one liner" functions often used with Underscore and as event handlers ... but occasionally, you'll see extra JavaScript code getting generated that is pointless.
Is there any way that you can generate developer friendly JavaScript when in Tapestry's development mode?. By developer friendly I mean try/catching js exceptions and rethrowing a wrapped exception containing the CoffeeScript line number / function name.
ReplyDelete@Unknown,
ReplyDeleteOn the client side, the more try/catch you do, the worse it is for developers, as JavaScript exceptions lose their context easily, and it becomes impossible to determine where the exception occurred.
That being said, a coming feature in browsers is JavaScript Source Maps, which do exactly what you want: map runtime line numbers in JavaScript back to source file/line in CoffeeScript (or whatever).
I appreciated this. I remember a comment in this article by Walter Bright saying that he planned for some redundancy in the D programming language to reduce errors of the type you discussed. In other words, there'll always be a tradeoff of terseness vs. debuggability.
ReplyDeleteRobert L. ("Les") Baker (don't know why it's showing up as "Unknown")