Uncle Bob: bowling meets Clojure

49 views
Skip to first unread message

Stuart Halloway

unread,
Jul 20, 2009, 12:27:17 PM7/20/09
to clo...@googlegroups.com
Uncle Bob Martin, a very well-respected OO and agile guy, is learning
Clojure. He has posted an example [1] and asked for feedback from the
Clojure community. I have made my suggestions in code [2] and will be
writing them up shortly.

Would love to see what other folks on this list have to say. If you
look at my version, it is worth walking through the commits one by one
to see the changes I made in going from an OO accent to more "Sequence-
Oriented Programming."

Cheers,
Stu

[1] http://blog.objectmentor.com/articles/2009/07/19/uncle-bob-jsps-learning-clojure
[2] http://github.com/stuarthalloway/clojure-bowling

Mark Engelberg

unread,
Jul 20, 2009, 2:01:31 PM7/20/09
to clo...@googlegroups.com
I think you guys are really overthinking this problem. Because
Clojure inherits Java's stack limitations, we tend to get hung up on
converting elegant recursive code into loop/recur/accumulator
structures. But here, we have a problem that we know isn't going to
blow the stack, so just think recursively and you end up with a
solution that is short and easy to understand:

(use 'clojure.contrib.test-is)

; A game is a sequence of numbers, representing how many pins were
knocked down for that roll

(defn sum [s] (reduce + s))
(defn score [game]
(cond
(< (count game) 3) (sum game),

(= (first game) 10) (+ (sum (take 3 game))
(score (drop 1 game))),

(= (sum (take 2 game)) 10)
(+ (sum (take 3 game)) (score (drop 2 game))),

:else (+ (sum (take 2 game)) (score (drop 2 game)))))

(deftest sample-games
(is (score [1 0 1 10 2 2 10 3 3 10 1 10 3 10 10 1 2]) 108)
(is (score [1 0 1 10 2 2 10 3 3 10 1 10 3 10 1 10 10 8 0]) 121)
(is (score [1 0 1 10 2 2 10 3 3 10 1 10 3 10 1 10 8 10 9]) 120))


;;;;;;;;;;;;
You can improve efficiency by enforcing an input of vectors and using
subvec, or you can keep the generality of sequences but change the
first test to something like (= (count (take game 3)) 3), but really,
why bother doing *anything* at the expense of clarity for a problem
where the inputs are so small that efficiency is a non-issue.

Laurent PETIT

unread,
Jul 20, 2009, 3:00:45 PM7/20/09
to clo...@googlegroups.com
Hi Stuart,

Couldn't your frames function benefit from using (when) instead of (if) (making it clear that the alternative is nil), and also, (when-let [rolls (seq rolls)]) instead of just (when rolls), which may be more robust and seems more idiomatic when used in conjunction with lazy-seq ?

BTW, I did on my laptop something similar to what you did, and then I read Mark's answer, and well, he has some good points also :-)


(defn frames
"Converts a sequence of rolls to a sequence of frames"
[rolls]
(if rolls
(lazy-seq (cons (take (balls-to-score rolls) rolls)
(frames (drop (frame-advance rolls) rolls)))))


2009/7/20 Stuart Halloway <stuart....@gmail.com>

Mark Engelberg

unread,
Jul 20, 2009, 5:20:16 PM7/20/09
to clo...@googlegroups.com
Whoops, I guess I don't understand bowling scoring as well as I
thought. Now that I've read up a bit more on bowling scoring, I see
that if you get down to three rolls, (say 10, 7, 2) it must be scored
differently depending on whether it is a strike in the 9th frame
followed by 2 balls in the 10th frame or just three balls from the
10th frame (in the first scenario, the second and third ball get
counted twice). So it looks like you really do need to assemble it
into a list of frame-by-frame scores.

Unfortunately, I misused the "is" macro (forgot to put =), which
prevented me from catching my incorrect tests.

The result is similar in spirit to Stuart's code, but arguably a bit
more compact.

(use 'clojure.contrib.test-is)

; A game is a sequence of numbers, representing how many pins were
knocked down for that roll

(defn sum [s] (reduce + s))

(defn frame-scores [game]
(cond
(< (count game) 3) [(sum game)],
(= (first game) 10) (cons (sum (take 3 game)) (frame-scores
(drop 1 game))),
(= (sum (take 2 game)) 10) (cons (sum (take 3 game))
(frame-scores (drop 2 game))),
:else (cons (sum (take 2 game)) (frame-scores (drop 2 game)))))
(defn score [games] (sum (take 10 (frame-scores games))))
(deftest sample-games
(is (= (score [6 1 9 0 8 2 5 5 8 0 6 2 9 1 7 2 8 2 9 1 7]) 127))
(is (= (score [10 10 7 3 8 2 10 9 1 10 10 10 10 7 3]) 232))
(is (= (score [10 10 7 3 8 2 10 9 1 10 10 10 7 2]) 210))
(is (= (score [5 5 8 2 9 1 7 3 8 2 6 4 9 1 7 3 6 4 4 5]) 163))
(is (= (score [10 10 10 10 10 10 10 10 10 10 10 10]) 300)))

Mark Engelberg

unread,
Jul 20, 2009, 5:26:03 PM7/20/09
to clo...@googlegroups.com
Sorry for the confusing choice of variable name. Should be "game" not
"games" in:
(defn score [game] (sum (take 10 (frame-scores game))))

Jim Oly

unread,
Jul 20, 2009, 2:16:44 PM7/20/09
to Clojure
My main concern was that the problem statement doesn't just specify
the score function but also a roll function to accumulate the ball
rolls. Obviously this will be different in Clojure since we would
prefer immutable data structures, but it feels like we're losing part
of the problem by ignoring roll altogether.

Jim

Mark Engelberg

unread,
Jul 20, 2009, 8:01:44 PM7/20/09
to clo...@googlegroups.com
Where is the original problem statement?

Stuart Halloway

unread,
Jul 20, 2009, 8:07:55 PM7/20/09
to clo...@googlegroups.com
Mark,

I think your approach is clean and simple, but loses the advantage of
decomposing the problem into useful constituent parts. I have written
this up in more detail at http://blog.runcoderun.com/post/145675117/tdd-in-a-functional-language-uncle-bobs-bowling
.

Stu

Mark Triggs

unread,
Jul 21, 2009, 3:00:06 AM7/21/09
to clo...@googlegroups.com
Hi Stu,


Stuart Halloway <stuart....@gmail.com> writes:

> Uncle Bob Martin, a very well-respected OO and agile guy, is learning
> Clojure. He has posted an example [1] and asked for feedback from the
> Clojure community. I have made my suggestions in code [2] and will be
> writing them up shortly.
>
> Would love to see what other folks on this list have to say.

My first version came out rather similar to yours, and then I started
thinking about turning the problem on its head and making the concept of
"types of rolls" more explicit. I'm still not sure how I feel about
this, but the line of thinking led me to code like this:

(ns bowling-game
(:use clojure.contrib.seq-utils))


(def *roll-types*
[{:name "strike"
:satisfies #(= (first %) 10)
:consumes 3
:advances 1}

{:name "spare"
:satisfies #(= (apply + (take 2 %))
10)
:consumes 3
:advances 2}

{:name "regular (underachieving?)"
:satisfies (constantly true)
:consumes 2
:advances 2}])


(defn roll-type [rolls]
(find-first #((:satisfies %) rolls)
*roll-types*))


(defn frames [rolls]
(when (seq rolls)
(let [{:keys [consumes advances]} (roll-type rolls)]
(cons (take consumes rolls)
(frames (drop advances rolls))))))


(defn score-game [rolls]
(reduce + (map #(reduce + %)
(take 10 (group-frames rolls)))))


Cheers,

Mark

--
Mark Triggs
<mark.h...@gmail.com>

Stuart Halloway

unread,
Jul 21, 2009, 9:03:31 AM7/21/09
to clo...@googlegroups.com
Very nice! I would change the :satisfies clause for underachievers so
that it does not create an ordering dependency.

You should post this over on Uncle Bob's site.

Stu

Jim Oly

unread,
Jul 21, 2009, 9:07:23 AM7/21/09
to Clojure
On Jul 20, 7:01 pm, Mark Engelberg <mark.engelb...@gmail.com> wrote:
> Where is the original problem statement?

It is hidden in Powerpoint slides (http://butunclebob.com/files/
downloads/Bowling%20Game%20Kata.ppt) linked on the description of the
bowling game (http://butunclebob.com/
ArticleS.UncleBob.TheBowlingGameKata). This may not be a big deal
since Uncle Bob did not include tests for roll but concentrated only
on score.

Jim

artg

unread,
Jul 21, 2009, 8:20:30 PM7/21/09
to Clojure
What is "group-frames"?

--art

On Jul 21, 12:00 am, Mark Triggs <mark.h.tri...@gmail.com> wrote:
> Hi Stu,
> <mark.h.tri...@gmail.com>

Mark Triggs

unread,
Jul 21, 2009, 8:37:55 PM7/21/09
to clo...@googlegroups.com
An artifact of not running my code from a clean REPL before posting ;)
It should just read `frames'.

Cheers,

Mark


artg <artgit...@gmail.com> writes:

> What is "group-frames"?
>
> --art
>
> On Jul 21, 12:00 am, Mark Triggs <mark.h.tri...@gmail.com> wrote:

[...]

>>   (defn score-game [rolls]
>>     (reduce + (map #(reduce + %)
>>                    (take 10 (group-frames rolls)))))

--
Mark Triggs
<mark.h...@gmail.com>

russellc

unread,
Jul 24, 2009, 11:40:41 AM7/24/09
to Clojure
FWIW (i.e. IMO the previous two functional solutions are better
examples) here is a more imperative style solution done sort of to
prove to myself that I could do such a thing in Clojure w/o too much
(arguable) fanfare. Maybe it will be interesting to others who are
learning Clojure too

(defn score [rolls]
(loop [roll-number 0 frame 0 score 0 frames []]
(if (or (= frame 10) (= roll-number (count rolls)))
frames
(let [roll (nth rolls roll-number)]
(if (= roll 10) ;; strike
(let [new-score (+ score roll (nth rolls (inc roll-number) 0) (nth
rolls (+ roll-number 2) 0))]
(recur (inc roll-number) (inc frame) new-score (conj frames
{:score new-score :frame-type :strike})))
(if-let [roll-2 (nth rolls (inc roll-number) nil)]
(if (= (+ roll roll-2) 10) ;;spare
(let [new-score (+ score roll roll-2 (nth rolls (+ roll-number
2) 0))]
(recur (+ roll-number 2) (inc frame) new-score (conj frames
{:score new-score :frame-type :spare})))
(let [new-score (+ score roll roll-2)]
(recur (+ roll-number 2) (inc frame) new-score (conj frames
{:score new-score :frame-type :underachieving?}))))
(let [new-score (+ score roll)]
(recur (inc roll-number) (inc frame) new-score (conj frames
{:score new-score :frame-type :incomplete})))))))))

(use 'clojure.test)

(deftest test-various-games
(are [description expected-score game] (= expected-score (:score
(last (score game))))
"gutter game" 0 (repeat 20 0)
"all ones" 20 (repeat 20 1)
"one spare" 16 (concat [5 5 3] (repeat 17 0))
"one strike" 24 (concat [10 3 4] (repeat 17 0))
"all nine pins first" 90 (conj (vec (interpose 9 (repeat 10
0) )) 9)
"all spares w/ gutter ball first" 100 (conj (conj (vec
(interpose 10 (repeat 10 0))) 10) 0)
"all spares w/ 1 pin first" 110 (conj (conj (vec (interpose 9
(repeat 10 1) )) 9) 1)
"all spares w/ 9 pins first" 190 (conj (conj (vec (interpose 1
(repeat 10 9) )) 1) 9)
"perfect game" 300 (repeat 12 10)
))

(run-tests)

On Jul 21, 8:37 pm, Mark Triggs <mark.h.tri...@gmail.com> wrote:
> An artifact of not running my code from a clean REPL before posting ;)
> It should just read `frames'.
>
> Cheers,
>
> Mark
>
> artg <artgittle...@gmail.com> writes:
> > What is "group-frames"?
>
> > --art
>
> > On Jul 21, 12:00 am, Mark Triggs <mark.h.tri...@gmail.com> wrote:
>
> [...]
>
> >>   (defn score-game [rolls]
> >>     (reduce + (map #(reduce + %)
> >>                    (take 10 (group-frames rolls)))))
>
> --
> Mark Triggs
> <mark.h.tri...@gmail.com>
Reply all
Reply to author
Forward
0 new messages