Permalink
Find file
b2b5c4a Feb 4, 2013
144 lines (98 sloc) 9.13 KB

Reusable Abstractions in CoffeeScript (and JavaScript!)

David Nolen made a really interesting observation:

Wow. HashMaps in ClojureScript are functions! Now this may look like some special case provided by the language but that's not true. ClojureScript eats its own dog food - the language is defined on top of reusable abstractions.

--Comparing JavaScript, CoffeeScript & ClojureScript

HashMaps in ClojureScript are functions. That's a really powerful idea. Why? Well, let's think about it. Right now, in CoffeeScript (and JavaScript), we have two things that behave in almost the same way but have different syntax:

a = b[c] # b is an array or an object

e = f(g) # f is a function

There is a good thing about this: The two different syntaxes signal to the reader whether we are dealing with arrays or functions.

There is a bad thing about this: None of the tools we develop for functions work with the syntax for arrays.

For example, we can compose any two functions with Underscore's compose function:

f = (x) -> x + 1
g = (x) -> x * 2

h = _.compose(f, g)
  # h(3) => f(g(3)) => 7

But we can't compose a function with an array reference:

a = [2, 3, 5, 7, 11, 13, 17, 19]
b = (x) -> x * 2

c = _.compose(a, b)
  # c(3) => TypeError: Object 2,3,5,7,11,13,17,19 has no method 'apply'

And this is just the beginning. You can .map over an array, but you can't pass an array to .map as an argument. Same for standard objects (a/k/a HashMaps). We can go though our toolbox, and find hundreds of places where we have a special tool for functions that we can't use on arrays or objects. How annoying. I suppose we could "monkey-patch" Array to support .apply and .call to get around some of these errors, but the cure would be worse than the disease.

Why CoffeeScript is an acceptable ClojureScript*

There are tremendous benefits to a language making these two things equivalent "all the way down." But for many practical purposes, we can reap the benefits of having arrays and objects be first-class functions by wrapping arrays and objects in a function. Here's one such implementation:

dfunc = (dictionary) ->
  (indices...) ->
    indices.reduce (a, i) ->
      a[i]
    , dictionary

dfunc takes an array or object you want to use as a "dictionary" and turns it into a function. So you can write:

address = dfunc
  street: "1010 Foo Ave."
  apt: "11111111"
  city: "Bit City"
  zip: "00000000"

And now you have a function, just like one of ClojureScript's use cases (try it!). dfunc is also useful for encapsulating the choice of using an object or array lookup for implementing a function. In Cafe au Life, rules for life-like games are represented as an array of arrays of neighbour counts. For example, the rules for Conway's Game of Life are represented as:

[
  [0, 0, 0, 1, 0, 0, 0, 0, 0, 0] # A cell in state 0 changes to state 1 if it has exactly 3 neighbours
  [0, 0, 1, 1, 0, 0, 0, 0, 0, 0] # A cell in state 1 changes to state 0 unless it has 2 or 3 neighbours
]

And the rules for the life-like game Maze are represented as:

[
  [0, 0, 0, 1, 0, 0, 0, 0, 0] # A cell in state 0 changes to state 1 if it has exactly 3 neighbours
  [0, 1, 1, 1, 1, 1, 0, 0, 0] # A cell in state 1 changes to state 0 unless it has 1 to 5 neighbours
]

Naturally, the code for actually processing the rules could use [] to look things up. But why should it know how the rules are represented internally? Instead, we wrap the rule array with dfunc,making a rule function out of an array representation of the rules, and from that, make a succ or "successor" function that computes the success for for any cell in a matrix of cells:

rule = dfunc [
  # ... rules for the current game ...
]

succ = (cells, row, col) ->
  current_state = cells[row][col]
  neighbour_count = cells[row-1][col-1] + cells[row-1][col] +
    cells[row-1][col+1] + cells[row][col-1] +
    cells[row][col+1] + cells[row+1][col-1] +
    cells[row+1][col] + cells[row+1][col+1]
  rule(current_state, neighbour_count)

Of course, succ could be written to depend on the array implementation of the rules. But turning it into a function factors it cleanly. We can change rule and succ independently, which is what we expect from encapsulating the array in a function.

And yet...

David Nolen also pointed out that encapsulating an array or object in a function isn't the same thing as having a language treat them as functions or even better, have them be made out of the same stuff. When you wrap an object inside of a function, you've hidden it, you lose access to everything about it except for the function's interface. Sometimes, that's exactly what you want. Much of software design is about modules exposing the right abstractions to their peers and clients.

And sometimes, that isn't what you want, and a language like ClojureScript effectively has it both ways: HashMaps are functions, so you can teat them as HashMaps and treat them as functions. And if you want to encapsulate them in a function so you can hide their HashMap nature, you can do that too. Brendan Eich suggests that Function Proxies are the right way forward for JavaScript (and CoffeeScript). This could get interesting once proxies are widely adopted.

Summary: Reusable Abstractions in CoffeeScript (and JavaScript)

Within a single function, it's good CoffeeScript and JavaScript to implement certain things with arrays and objects as dictionaries. Naturally! But when exposing properties as part of an API, functions are preferred, because functions are more easily reused abstractions and they preserve a read-only contract. JavaScript and CoffeeScript don't actually implement arrays and HashMaps as functions, but it's easy to wrap them in a function and obtain many of the benefits.

(discuss)

p.s. About Cafe au Life. From time to time, I write a post and include overly simple examples. Such examples help to highlight the techniques being discussed by ruthlessly doing away with the accidental (for the purpose of the technique) complexity of real-world code. I've decided to take a different tack for a while. I plan to use Cafe au Life as my standard code base for CoffeeScript examples. My hope is that it plays out as follows: The examples will be a less obvious because readers have to figure out a little about implementing Life recursively, and there will be opportunities to derail discussions by pointing out poor choices I've made that have nothing to do with a particular post. But in return, discussions have the potential to be richer for being set in the context of trying to implement a beautiful algorithm, and any digressions will be fortuitous and productive.

p.p.s * I kid, I kid. Don't take the flame-bait!


My recent work:

JavaScript AllongéCoffeeScript RistrettoKestrels, Quirky Birds, and Hopeless Egocentricity


(Spot a bug or a spelling mistake? This is a Github repo, fork it and send me a pull request!)

Reg Braithwaite | @raganwald