Jacques Mattheij

Technology, Coding and Business

Decoding Clojure

This is a terrible mangling of a piece of code by Eric Lavigne (thanks!), I take full responsibility for any lines starting with and the afterword.


I’m a total newbie when it comes to clojure, so when I got my hands on a little clojure program to play the game of ‘island-wari’ I went and dissected it bit by bit, clojure manual in hand to see how it works.


The original code did not contain any comments at all so this was a both trying to figure out what the langauge does and what the program does.


// // This all started here: http://news.ycombinator.com/item?id=1043180 // // To ‘recover’ the original clojure source simply do // ‘grep -v //’ on this file.
// // An explanation of the game of ‘island wari’ is here: // http://waynesword.palomar.edu/ww0603.htm // // In short, it is a two player game with 7 ‘slots’ for each // player, the rightmost slot for a player (positions 6 and 13) // is called the homebase, the players take turns and indicate // a bin they wish to play, stones then get distributed // to all bins counterclockwise of the bin played. If the last // stone lands on the players homebase that player gets to play // again, players have to skip the homebase of their opponent // when placing stones. Whoever has their side of the board // empty first wins the game.
// // The ‘pseudo-C’ language equivalents for the shorter functions are // there simply because I tried to translate the functions in // to something that I can readily understand, they are not // used anywhere (and won’t even compile) //

// this sets up the namespace and imports compojure

(ns wari (:use compojure) (:gen-class))

//////////////////////////////////////////////////////////////////////////////////

// return a list of board positions where stones will be placed // given a starting position and an indication which ‘home base’ // should be avoided, as well as the number of stones that
// will need to be placed

(defn where-stones-fall start skip stones)

// (range 14) becomes: // (0 1 2 3 4 5 6 7 8 9 10 11 12 13) // (cycle (range 14)) yields:
// (0 1 2 3 4 5 6 7 8 9 10 11 12 13 0 1 2 3 4 5 6 7 8 9 10 11 12 13 etc ) // (inc start) is
// start + 1
// so
// (drop (inc start) (cycle (range 14)))
// gives
// the elements from the cycle found from start+1 onwards
// so if start is 5 then this will yield:
// ( 5 6 7 8 9 10 11 12 13 0 1 2 3 4 5 6 7 8 9 10 11 12 13 etc )
// (we start at ‘5’ becaue the original range started at 0, the first
// 5 elements are then 0,1,2,3,4).
// now we are at the remove call, which looks like this:
// (remove #(= skip %) (5 6 7 8 9 10 11 12 13 0 1 2 3 4 5 6 7 8 9 10 11 12 13 etc )) // assuming a start of 5
// bin to skip can be
// 6 or 13 depending on whose move it is
// so the remove call will take the ‘skip’ entry and remove it from the list
// if ‘skip’ was 6 then the list now looks like this:
// (5 7 8 9 10 11 12 13 0 1 2 3 4 5 7 8 9 10 11 12 13 etc )
// now we are at the outer braces:
// (take stones (5 7 8 9 10 11 12 13 0 1 2 3 4 5 7 8 9 10 11 12 13 etc ))
// this will return a list of the first ‘stones’ entries from
// the colection
// for ‘stones’ is 10 this would return:
// (5 7 8 9 10 11 12 13 0 1)

// the relevant clojure built-in functions used (from the clojure // documentation):

// (take n coll) // Returns a lazy sequence of the first n items in coll, or all items if // there are fewer than n.

// (remove pred coll) // Returns a lazy sequence of the items in coll for which // (pred item) returns false. pred must be free of side-effects.

// (drop n coll) // Returns a lazy sequence of all but the first n items in coll.

// (inc x) // Returns a number one greater than num.

// (cycle coll) // Returns a lazy (infinite!) sequence of repetitions of the items in coll.

// (range end) // Returns a lazy seq of nums from 0 (inclusive) to end (exclusive)

//////////////////////////////////////////////////////////////////////////////////

// figure out which bin to skip, this passes the opponents ‘home base’ // bin

(defn bin-to-skip turn)

// bin-to-skip(turn) { if (turn == 1) return 13 else return 6 }

//////////////////////////////////////////////////////////////////////////////////

// the bin to play again depends on who is playing, it is equal to the // homebase bin for the current player

(defn bin-to-play-again turn)

// bin-to-play-again(turn) { return bin-to-skip(turn == 1 ? 2 : 1) }

//////////////////////////////////////////////////////////////////////////////////

(defn alter-board board turn move
)

// alter-board returns the board as it is after the move done by the player whose // turn it is. First ‘stones’ is set to become a list of board positions that
// will be receving a stone.

// I fiddled a bit with the parentheses because it wasn’t entirely clear to me // which belonged to which, and which part constituted the body of the function // argument to ‘map’. So, map appears to receive 3 arguments here, the function // body, and two collections. The first collection is the current board, the
// second collection is the numbers (0 … 13).

// A quick check outside in the repl: // (map (fn i) [0 1 2 3 4 5]) // returns
// (0 5 10 15 20 25)
// but strangely
// (map (fn i) (0 1 2 3 4 5)) // fails with a ‘java.lang.ClassCastException: java.lang.Integer cannot be cast to // clojure.lang.IFn (NO_SOURCE_FILE:0)’
//
// Why is it that map only works on vectors and not on lists ?
// Ah, solved that one the (0 1 2 3 4 5)
// does not work as a list parameter in that spot because ‘0’ is
// seen as the function name. Probably a classic newbie mistake,
// I’m expecting here to pass a list of numbers but to do that you
// have to make a list. The [] notation of the vector does not
// deal with this in the same way because the vector does not start
// with an ‘(’ indicating the next token is a function name.
//
// Stupid me :)
//
// So, the list example then becomes:
// (map (fn i) (list 0 1 2 3 4 5))
//
// where ‘list’ will return the remaining arguments as a list.
// And sure enough, that works.

// map then returns // (fn [b i]
// (+ (if (= move i) 0 b) // (count (filter #(= i %) stones)) // )
// )
//
// fn is then applied to each of the counts of stones in each bord position // (in parameter ‘b’) and the rank of the position (in parameter ‘c’),
// stones and move are also ‘in scope’.
//
// That little anonymous function then does the following:
// If the position under consideration is the one that is
// being played it gets set to contain ‘0’ stones, otherwise
// we start off with the number of stones found there
// that takes care of the ‘(if (= move i) 0 b)’, then the
// ‘(filter #(= i %) stones)’ expression returns the
// collection consisting of ‘nil’ in case the current board
// position is not in the list of board positions to receive
// a stone and a collection of one element, the board position
// if it is to receiave a stone.
//
// count then counts the number of elements in the collection
// (0 or 1) and will add that to the number of stones already
// there or to 0 in the case of the position being played.
//

//////////////////////////////////////////////////////////////////////////////////

(defn next-turn board turn move)

// // this calls where-stones-fall to get a list of the slots where stones will // be placed, the (bin-to-skip turn) will return 6 or 13 depending on whose turn // it is, the (nth board move) will return the number of stones in the
// board position that is being played.
//
// This is where I don’t think ‘lisp’ like languages are more ‘expressive’ than // many other languages, after all:
//
// array[12];
//
// vs
//
// (nth array 12)
//
// hardly seems an improvement, but I think anybody proficient in lisp
// would probably read this with the same ease. It just feels as though
// the ‘functional’ technique is taken a little further than is practical
// here. Of course, having only one technique also has a certain elegance
// but I think that other languages have an advantage here. Is there a
// lisp that supports subscripted arrays with square brackets ? That would
// also take care of passing ‘nth’ anything other than a vector (which
// leads to some pretty cryptic error message)
//
// The ‘let’ function binds the ‘last-stone’ symbol to the last entry of
// the list of board positions where stones will be placed, if the
// last stone is placed in the home base of the player that just played
// then that player will get to play again, otherwise the other player
// will play next

//////////////////////////////////////////////////////////////////////////////////

(defn move-result [board turn move] {:pre [(or (= turn 1) (= turn 2)) (or (and (= turn 1) (< move 6)) (and (= turn 2) (> move 6) (< move 13))) (< 0 (nth board move))]}
{:turn (next-turn board turn move)
:board (alter-board board turn move)})

//////////////////////////////////////////////////////////////////////////////////

(defn board-route turn board)))

// board-route(turn,board) { return “/” + turn + “/0/” + implode(“/”,board) }

//////////////////////////////////////////////////////////////////////////////////

// render board displays the board the player has chosen to continue // the game with, and creates links for the new board if a position // is ‘playable’, clicking on a position then effects the next move

// the ‘whosturn’ variable makes sure the right side of the board // has the links placed.

(defn render-board [whosturn slots] {:pre [(or (= whosturn 1) (= whosturn 2))]} (let [vert [:img {:src “/vertical.png”}]
horiz [:img {:src “/horizontal.png”}] small [:img {:src “/small.png”}]
big [:img {:src “/big.png”}]
top [:img {:src “/top.png”}]
bot [:img {:src “/bottom.png”}]
picname-to-imgcol #(vector :td [:img {:src (str “/” % “.png”)}]) slot-to-link
(fn move “.png”)}]]]))]
[:table {:width “50%”}
[:tr (map #(vector :td %) (interpose horiz (repeat 7 small)))]
[:tr :td vert (picname-to-imgcol “vertical”)
(= (nth slots slot) 0) (picname-to-imgcol 0)
(= whosturn 2) (picname-to-imgcol (nth slots slot))
:default (slot-to-link slot)))
(interpose “vertical” (reverse (range 6))))
[:td vert]]
[:tr [:td vert] :td top
[:td top] [:td vert]]
[:tr :td vert
(map #(vector :td %) (interpose big (repeat 5 vert)))
(picname-to-imgcol (nth slots 13)) [:td vert]]
[:tr [:td vert] :td bot
[:td bot] [:td vert]]
[:tr :td vert (picname-to-imgcol “vertical”)
(= (nth slots slot) 0) (picname-to-imgcol 0)
(= whosturn 1) (picname-to-imgcol (nth slots slot))
:default (slot-to-link slot)))
(interpose “vertical” (range 7 13)))
[:td vert]]
[:tr (map #(vector :td %) (interpose horiz (repeat 7 small)))] ]))

// this routine is uncharacteristically long! //
// It creates a table with 13 columns x 7 rows, the outer ones are // filled with ‘small.png’ in the corners, ‘vertical.png’ on the left // and right edge and ‘horizontal.png’ on the top and bottom edges.
//
// For each of the players their respective ‘bins’ are displayed,
// with up to 48 stones per ‘bin’.
//
// This job would be much better suited to a templating engine
// of sorts, which could be passed a vector containing the
// number of stones for each position as well as an optional link
// for that position in case it is played.

//////////////////////////////////////////////////////////////////////////////////

// // the despatcher, it matches incoming requests to methods and urls //

(defroutes wari-routes (GET “/
(or (serve-file (params :
)) :next)) (GET “/”
(html [:h1 “Welcome to the Island Wari game server.”] [:a {:href (board-route
1
(apply concat (repeat 2 (concat (repeat 6 4) [0]))))} “New game”]))
(GET “/:whosturn/:computerplays/:x/:x/:x/:x/:x/:x/:x/:x/:x/:x/:x/:x/:x/:x”
(let turn (Integer/parseInt (params :whosturn))
board (map #(Integer/parseInt %) (params :x))
)) (ANY “*” (page-not-found)))

// the despatcher tries to match each of the routes in turn until it // finds one that returns a non-nil result. So the first thing tried // is to serve up a static file. If that fails a check is made to see // if the homepage of the server is requested, the homepage is output // ‘inline’ to the client. // // Then a move is tried, and if that matches the turn and board // variables are recoverd from the request resource and if the // turn variable is either 1 or 2 a new board is genereated // // finally, if no match can be made page-not-found is called which // will display compojures standard 404 page.

//////////////////////////////////////////////////////////////////////////////////

(defn -main & args))

// this calls the run-server function with a port number, a match template // and a servlet to handle the various requests

//////////////////////////////////////////////////////////////////////////////////

Afterword:

What strikes me as clever is the use of ‘lazy’ evaluation to return constructs like infinite lists (or actually, just as much as you’ll use).

I’m still very much in ‘braces’ hell, it is going to take me a while before I can read this stuff quickly.

Is there some kind of templating system to use with compojure? I’d hate to give this code to a designer to make the html it outputs look good, especially if the site would have many pages with a standardized lay-out that might prove to be a serious problem.