2013-06-14

CoffeeScript - Second Impressions

Here's a small raycaster in CoffeeScript. I wrote that just to see how it would look. (There's also a small rant at the end, if that's what you're looking for.)

DIST = 9; Z_STEP = 0.1; X_STEP = 2; FOV = Math.PI/3

map_at = (x, y) -> [
    " XX"
    "   "
    "X X"
    ][Math.round y]?[Math.round x] == "X"

move = (x, y, dist, dir) -> [Math.sin(dir) * dist + x,
                             Math.cos(dir) * dist + y]

cast = (x, y, dir) -> # optimizing for LOC, here...
    for dist in [0...DIST] by Z_STEP
        break if map_at (move x, y, dist, dir)...
    dist

clear = (canvas) ->
    ctx = canvas.getContext '2d'
    ctx.fillStyle = "#fff"
    ctx.fillRect 0, 0, canvas.width, canvas.height

draw = (canvas, col, dist) ->
    dist_frac = Math.min(dist, DIST)/DIST
    wall = (1-dist_frac) * canvas.height
    color = Math.round dist_frac*255
    ctx = canvas.getContext '2d'
    ctx.fillStyle = "rgb(#{color}, #{color}, #{color})"
    ctx.fillRect col, canvas.height/2 - wall/2, X_STEP, wall

render = (canvas, player) ->
    clear canvas
    for col in [0..canvas.width] by X_STEP
        view_col = (col - canvas.width/2)
        view_dir = view_col*(FOV/canvas.width)
        draw canvas, col,
             cast player.x, player.y, player.dir + view_dir

running = false
frame = ->
    clearInterval running
    cycle = new Date().getTime()/1000
    render document.getElementById('canvas'),
             x:   Math.sin(cycle)*6 + 1
             y:   Math.cos(cycle)*6 + 1
             dir: cycle + Math.PI # look into center
    running = setInterval frame, 20

do frame

Result (click through from your RSS reader if you can't see a canvas):

Not bad, certainly more succinct than JavaScript, but ...well - maybe I've been writing too much Scheme lately but I've grown to like having parentheses around everything. Making most of the nesting implicit may look cleaner, but it feels so brittle to me. One basically relies on operator precedence, and - somehow - that's making me uncomfortable. (To be fair - I could have included much more parentheses in the above code, I just thought I'd try and go full-on crazy with CoffeeScript's syntax).

But the biggest problem I have with these significant-whitespace and implicit-nesting languages is that they don't go far enough: If I was to write something like

 foo = bar x,y
       * factor

or even

foo = bar x,y  * factor

a human reader would probably assume it means

foo = bar(x,y) * factor

But CoffeeScript parses the second one as

foo = bar(x,y*factor)

and errors-out on the first one. In the same vein, this works in CoffeeScript:

coffee> (x for x in [0..3] when x != 2)
[ 0, 1, 3]

But this doesn't:

coffee> (x for x in [0..3] unless x == 2)
[ 0, 1, 2, 3 ]

even though unless is a keyword and behaves as one would expect in most other cases.

coffee> x = 1 unless true is false
1

I mean, one can go down the highway to hell road of "code that looks like English sentences", but one has to go all the way, with a real natural language parser. But what all of these languages - including CoffeeScript - do is rename "&&" to "and" and replace some other symbols with "then" and "when". So now stuff looks like English but behaves like code - including strange parse errors. How does that help anyone?

That's what I meant when I said "90% design" in the last post - it just doesn't fucking work all of the time, and that's frustrating. One man's "why would you do that?" is another man's "basic usage".

x = 1 if true      # works

if true x = 1      # doesn't

if true then x = 1 # works

if true then       # doesn't work
    x = 1          # (parse error)

if true then       # "works"
x = 1              # (always sets x to 1)

if true            #
    x = 1          # works

if true and true   #
    x = 1          # works

if true            #
and true           # doesn't work
    x = 1          # (parse error)

if true            #
   and true        # doesn't work
    x = 1          # (different parse error)

if true and        #
   true            #
    x = 1          # works

if true and        #
    true           #
    x = 1          # doesn't (!)

if true and        #
     true          #
    x = 1          # works (!!!!!)

Python would have the same problem, if it wasn't for the fact that stuff in parentheses is free-form there - but no such luxury in CoffeeScript.

Still, all-in-all I find CoffeeScript rather nice - it has quite a law-and-order syntax but the execution model is unchanged from JavaScript (i.e. nice and free). Simple things - that is, "real-world code" - seem to look better in CoffeeScript. And (big plus) it compiles into straight-forward, readable JavaScript.

It does leave many of JavaScript's warts unfixed (I assume this is by design), but yeah, at least everything is an expression now and the scope behaves as it should.



comments powered by Disqus