Skip to content

Instantly share code, notes, and snippets.

@jedschneider
Last active December 20, 2015 10:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jedschneider/6113442 to your computer and use it in GitHub Desktop.
Save jedschneider/6113442 to your computer and use it in GitHub Desktop.
Another Take on 'I hated, hated, hated this CoffeeScript'

Another Take on 'I hated, hated, hated this CoffeeScript'

As a follow up to both the initial article and the followup, I wanted to give my own take on 'idiomatic CoffeeScript' as I see it. I'm a huge fan of Reggie's work and highly reccomend his books on CoffeeScript and JavaScript.

I like CoffeeScript's class syntax, I admit it. Not because I love OOP or think that we should necessarily think in in OOP concepts, but because it provides a clean functional interface for thinking in domain concepts. Classes in CoffeeScript are not classes, they are function wrappers with their own environments. If you want to think of them as classes, have at it, I won't blame you.

To review for Reggie's article, lets look at his concept of idiomatic CoffeeScript.

table = (numberOfRows, numberOfColumns) ->
  row = (numberOfColumns) ->
    "<tr>#{ ('<td></td>' for i in [1..numberOfColumns]).join('') }</tr>"
  "<table>#{ (row(numberOfColumns) for i in [1..numberOfRows]).join('') }</table>"

table(3,3)
  #=> "<table><tr><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td></tr></table>"

My biggest issue with this code is that its hard to tween apart when you read it. There's a lot going on there. Cool, yes. Idiomatic, yes, there are lots of idioms in there, but idiomatic, I'm not sure. Thinking from a test driving persepctive, extending this function to do something a little different or changing the functionality means reeding between the lines of nested functional calls and constructs. Let me suggest a different model:

class Table
  constructor: (@rows, @columns)->

  toHtml: ->
    table_body = @wrap (@createRow() for row in [0...@rows]), 'table'
    table = []
    table.concat.apply(table, table_body).join("")

  createRow: ->
    createCell = -> "<td></td>"
    @wrap (createCell() for cell in [0...@columns]), 'tr'

  wrap: (content, el)->
    clone = content
    clone.unshift("<#{el}>")
    clone.push("</#{el}>")
    clone

table = new Table(3,3).toHtml()

Writing this class was fairly simple. I could build my code up from a single function that creates the cell to then the row and finally the table itself. You'll see all of Reggie's suggested idioms in there, and maybe some more (personally I like the assignment of 'instance variables' in the constructor), but at a glance not only is this code clear, its clearly ogranized into helper functions that all do their part. Building maintainable code starts with abstractions that are in themselves maintainable. There are some things not to like here, especially if you perfer Reggie's functional style where there are not several public methods available. If privacy and scoping are important to you (and they should be sometimes) you can refactor this Table function (aka class) into more of a constructor function that represents a 'functional core, imperative shell' interface.

class Table
  constructor: (@rows, @columns)->

  toHtml: ->
    wrap = (content, el)->
      clone = content
      clone.unshift("<#{el}>")
      clone.push("</#{el}>")
      clone

    createRow = =>
      createCell = -> "<td></td>"
      wrap (createCell(cell) for cell in [0...@columns]), 'tr'

    table_body = wrap (createRow() for row in [0...@rows]), 'table'
    table = []
    table.concat.apply(table, table_body).join("")

table = new Table(3,3).toHtml()

I will say one more thing that I like about this solution more, mainly that the 'html' stays as data for much longer than in the case where Reggie is joining into a string through each level of the function. Here, the represntation of the HTML remains as a nested set of arrays until the end of the toHtml function, so being able to dynamically modify the data structure with other array elements is 'baked in'. To see why that might be cool, check out my favorite templating library (in concept) hiccup-js.

To me one the the best parts about CoffeeScript is being able to abstract the notion of the domain into constructs that better represent the idea in clarity and behavior. For the last year I've been exploring these ideas of domain objects, encapsulating behavior and state to the object, and operating interally to the object in a functional state. And I'm really enjoying the process. Right now its my hammer, and everything is a nail.

@gvarela
Copy link

gvarela commented Jul 30, 2013

So the main issue I have with keeping the information as data until the end is that the information you are keeping is still effectively rendered strings. This sort of defeats the purpose of keeping your data until the end. A more concise version that would be easier to test and is more OO would be more like

class Table
  constructor: (@rows, @columns)->

  toHtml: ->
    @el 'table', (@renderRow() for row in [0...@rows]).join('')

  renderRow: ->
    @el 'tr', (@renderCell() for cell in [0...@columns]).join('')

  renderCell: ->
    "<td></td>"

  el: (el, content)->
    "<#{el}>#{content}</#{el}>"

table = new Table(3,3).toHtml()

I think the issue with trying to pick apart Reggie's example is that it is purposefully overly simplified to prove a point about the difference between js and coffeescript.

@jedschneider
Copy link
Author

I hope I didn't sound like I was picking on Reggie. Just another way to see the problem. Thanks @gvarela, I love the simplicity you provided to the idea.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment