In software development there are no silver bullets, but I am always looking out for the next bronze one.

Tuesday, February 10, 2009

Bowling with Clojure

Having an interest in function programming languages (notably Scala), I decided to take a look at Clojure. In a nutshell, Clojure is a LISP variant that runs on the Java Virtual Machine and hence enjoys access to all libraries therein.
One of my favorite simple code katas with functional languages is to tally up bowling scores. Previously, I had done this with Erlang and Scala. Here is a Clojure version of the same including some tests:
(defn score-frame [rolls]
(let [[x1 x2 x3] rolls]
(cond
(nil? x1) nil
(nil? x2) nil
(nil? x3) (let [sumx (+ x1 x2)]
(if (< sumx 10) (list sumx))) ; open at end of game
(= x1 10) (cons (+ x1 x2 x3) (rest rolls)) ; strike
(= (+ x1 x2) 10) (cons (+ x1 x2 x3) (nthrest rolls 2)) ; spare
(< (+ x1 x2) 10) (cons (+ x1 x2) (nthrest rolls 2)) ; open
(true) nil
)
)
)

(defn roll-reduce [remaining-rolls]
(if (not (= nil remaining-rolls))
(let [result (score-frame remaining-rolls)]
(if (not (= nil result))
(cons (first result) (roll-reduce (rest result))))))
)

(defn score-ten-frames [rolls]
(take 10 (roll-reduce rolls)))

(defn tally-game [accum scores]
(if (> (count scores) 0) (let [new_accum (+ accum (first scores))] (cons new_accum (tally-game new_accum (rest scores)))))
)

(defn score-game [rolls]
(tally-game 0 (score-ten-frames rolls))
)

(assert (= '(30 60 90 120 150 180 210 240 270 300) (score-game '(10 10 10 10 10 10 10 10 10 10 10 10))))
(assert (= '(30 60 90 120 150 180 210 240 270 299) (score-game '(10 10 10 10 10 10 10 10 10 10 10 9))))
(assert (= '(30 60 90 120 150 180 210 240 270 290) (score-game '(10 10 10 10 10 10 10 10 10 10 10 0))))
(assert (= '(20 50 80 110 140 170 200 230 260 290) (score-game '(9 1 10 10 10 10 10 10 10 10 10 10 10))))
(assert (= '(30 60 90 120 150 180 210 240 269 289) (score-game '(10 10 10 10 10 10 10 10 10 10 9 1))))
(assert (= '(30 60 90 120 150 180 210 240 269 288) (score-game '(10 10 10 10 10 10 10 10 10 10 9 0))))
(assert (= '(30 60 90 120 150 180 210 240 260 280) (score-game '(10 10 10 10 10 10 10 10 10 10 0 10))))
(assert (= '(20 40 70 100 130 160 190 220 250 280) (score-game '(10 9 1 10 10 10 10 10 10 10 10 10 10))))
(assert (= '(30 60 90 120 150 180 210 239 259 279) (score-game '(10 10 10 10 10 10 10 10 10 9 1 10))))
(assert (= '(30 60 90 120 150 180 210 240 260 270) (score-game '(10 10 10 10 10 10 10 10 10 10 0 0))))
(assert (= '(20 40 60 80 100 120 140 160 180 200) (score-game '(10 9 1 10 9 1 10 9 1 10 9 1 10 9 1 10))))
(assert (= '(19 38 57 76 95 114 133 152 171 190) (score-game '(9 1 9 1 9 1 9 1 9 1 9 1 9 1 9 1 9 1 9 1 9))))
(assert (= '(19 38 57 76 95 114 133 152 171 180) (score-game '(9 1 9 1 9 1 9 1 9 1 9 1 9 1 9 1 9 1 9 0))))
(assert (= '(0 0 0 0 0 0 0 0 0 0) (score-ten-frames '(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))))

; Partial games
(assert (= nil (score-game nil)))
(assert (= nil (score-game '(0))))
(assert (= nil (score-game '(9))))
(assert (= nil (score-game '(10))))
(assert (= nil (score-game '(9 1))))
(assert (= nil (score-game '(10 10))))
(assert (= '(0) (score-game '(0 0))))
(assert (= '(1) (score-game '(0 1))))
(assert (= '(9) (score-game '(8 1))))
(assert (= '(9) (score-game '(9 0))))
(assert (= '(19) (score-game '(9 1 9))))
(assert (= '(19 28) (score-game '(10 9 0))))
(assert (= '(19 28) (score-game '(9 1 9 0))))
(assert (= '(20) (score-game '(10 9 1))))
(assert (= '(30) (score-game '(10 10 10))))
(assert (= '(30 59) (score-game '(10 10 10 9))))
(assert (= '(30 59 78 87) (score-game '(10 10 10 9 0))))


Note this code has been updated since initial posting to incorporate changes based on feedback in a comment from Mark Volkmann below. Mark has published an excellent overview of Clojure here, http://www.ociweb.com/jnb/jnbMar2009.html


Link to code


Example:
C:\lang\clojure>java -jar clojure.jar bowling.clj
Clojure
user=> (score-game '(10 10 10 10 10 10 10 10 10 10 10 10))
(30 60 90 120 150 180 210 240 270 300)
user=> (score-game '(10 10 10 10 10 10 10 10 10 10 10 9))
(30 60 90 120 150 180 210 240 270 299)
user=> (score-game '(10 10 10 10 10 10 10 10 10 10 9 1))
(30 60 90 120 150 180 210 240 269 289)
user=> (score-game '(10 10 10 10 10 10 10 10 10 9 1 10))
(30 60 90 120 150 180 210 239 259 279)
user=> (score-game '(10 10 10 10 10 10 10 10 10 9 0))
(30 60 90 120 150 180 210 239 258 267)
user=> (score-game '(9 0 10 10 10 10 10 10 10 10 10 10 10))
(9 39 69 99 129 159 189 219 249 279)

3 comments:

Mark Volkmann said...

I'm working on simplifying this. One thing you can do is replace the first function with the following. Note that Clojure convention is to make multi-word names all lowercase with words separated by hyphens.

(defn score-frame [rolls]
(let [[x1 x2 x3] rolls
sum2 (when (and x1 x2) (+ x1 x2))
sum3 (when (and sum2 x3) (+ sum2 x3))]
(cond
(nil? x1) nil
(nil? x2) nil
(nil? x3) (if (< sum2 10) (list sum2)) ; open at end of game
(= x1 10) (cons sum3 (score-frame (rest rolls))) ; strike
(= sum2 10) (cons sum3 (score-frame (nthrest rolls 2))) ; spare
(< sum2 10) (cons sum2 (score-frame (nthrest rolls 2))) ; open
true nil)))

zak said...

I think this is a more idiomatic version of scoreFrame (sorry about the formatting, but Blogger won't let me use pre or code tags):

(defn scoreFrame [rolls]
(let [[x1 x2 x3] rolls]
(cond (or (nil? x1) (nil? x2)) nil
(nil? x3) (let [sumx (+ x1 x2)]
(if (< sumx 10) (list sumx))) ; open at end of game
(= x1 10) (cons (+ x1 x2 x3) (rest rolls)) ; strike
(= (+ x1 x2) 10) (cons (+ x1 x2 x3) (drop 2 rolls)) ; spare
(< (+ x1 x2) 10) (cons (+ x1 x2) (drop 3 rolls)) ; open
true nil)))

Some tips:

Clojure let does destructuring - no need to access members of a sequence individually in cases like this.

There's probably no need to bind a symbol to each of the segments of rolls that you want - just access them with drop.

The true at the end of the cond clauses doesn't need to be bracketed. In fact, I don't know why it isn't throwing an exception (trying to call a boolean as a function).

Traditional Lisp style is to put all the closing parens on the same line. I know that seems very odd coming from other languages, but it's the way everyone in the Lisp world does things.

I hope you're enjoying Clojure!

Anonymous said...

hye, i browsed for a bowling score coding today.
and found ur blog.
can i just ask how do we do the coding to display the output neatly?
&& do we need to prompt the user to enter the score?