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.
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
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
.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
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)
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
succ independently, which is what we expect from encapsulating the array in a function.
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.
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:
- jQuery Combinators, what else? A jQuery plugin for writing your own fluent, jQuery-like code.
(Spot a bug or a spelling mistake? This is a Github repo, fork it and send me a pull request!)