The RunCodeRun Blog

Jul 20 2009

TDD in a functional language: Uncle Bob’s Bowling

Want to see what TDD is like in a functional language? In this blog I will convert the Bowling Game example, as TDD’ed by Uncle Bob, to a more functional approach in Clojure.

Uncle Bob Martin, well-known OO and agile guy, has used the Bowling Game to explore TDD in a variety of different languages. Most recently, he has been learning Clojure, and experiencing a disconnect from the idioms he would use in an OO language. I felt this same disconnect when I started using Clojure, and I think I have finally bent my brain into the shape needed to TDD functional code.

I have taken Uncle Bob’s example, and converted it to a more functional approach while keeping tests passing at each step. This blog summarizes the key elements of my thinking, but of course you can also follow it step by step in the commit history of the clojure-bowling repository.

A starting point

The initial commit of the project simply imports Uncle Bob’s code, and adds some supporting infrastructure (shell scripts to launch a REPL with all the code available, etc.)

In addition, I chose to use the development edge of Clojure and clojure-contrib to take advantage of the new syntax for the are macro (more on that later). The repository includes compatible versions of these JAR files so you don’t have to download anything else.

Data, not objects

In OO, it is customary to invest a lot in how objects get made. So, the code includes functions like new-game that creates a game. This starts to get ugly with the functions that make test data. For example, the roll-many function creates many identical rolls of a bowling ball, and the roll-list takes an arbitrary list of rolls and adds them to the game. Using these functions leads to some pretty hard-to-read tests. For example, here is one that tests scoring a game that starts with a spare, followed by all gutterballs:

(deftest one-spare
  (is (= 16 (score
    (roll-many
      (roll-list (new-game) [5 5 3])
      17 0)))))

Ouch. I can hear the parenthesis-haters sharpening their pitchforks in the background. But what happens if we think of the game as a simple sequence of data?

"one spare" 16 (concat [5 5 3] (repeat 0))

In this tabular-looking expression, the "one spare" names the case being tested, the 16 is the expected score, and the concat builds the list of rolls: 5, 5, 3, 0, 0, 0 … Note that we are taking advantage of Clojure’s lazy collections to ignore exactly how many zeroes are needed.

The tabular, FIT-like style of listing values is enabled by the are macro. There are tests for several different possible games, and it is easy to add more:

(deftest test-various-games
  (are [description expected-score game] (= expected-score (score game))
       "gutter game" 0 (repeat 0)
       "all ones" 20 (repeat 1)
       "one spare" 16 (concat [5 5 3] (repeat 0))
       "one strike" 24 (concat [10 3 4] (repeat 0))
       "perfect game" 300 (repeat 10)
       ))

Recursion: a misstep

Scoring bowling is simple: “iterate over the frames of the game, adding the score for each.” Since we are using functional style, we will use recursion instead of iteration. Furthermore, since we are on the JVM, we will use Clojure’s explicit recur to trigger Tail-Call Optimization (TCO). Here is the original:

(defn score [game]
  (loop [frame 1 rolls game score 0]
    (if (> frame 10)
      score
      (let [frame-score (score-next-frame rolls)]
        (recur (inc frame) (subvec rolls (frame-score 0)) 
               (+ score (frame-score 1)))))))

That works, but I think I hear the parenthesis-bashers giggling again. The score function is pretty tough to read, especially the loop with five positional arguments. What is more troubling is that this function relies on a supporting infrastructure of five other functions, most of which don’t make much sense alone. Here is score-spare:

(defn score-spare [rolls]
  [2 (+ 10 (rolls 2))])

After puzzling a minute, it becomes clear: score-spare returns a pair:

  • the number of balls consumed by a spare frame
  • the score for this specific spare frame (ten plus the extra ball in the next frame).

Now I can hear the OO crowd sharpening their spears: “With objects it would be clear what was being returned here: the FrameStat bean or some such.”

Sequence transformation!

Instead of thinking about iterating over the rolls, think about transforming the rolls into a sequence of the thing you really want: the frames. So, for example:

; balls        => frames
[10 1 4 5 5 3] => [[10 1 4] [1 4] [5 5 3]]

The new sequence includes in each frame the balls needed to score that frame. Here is an implementation:

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

This function grabs (balls-to-score rolls) to make a frame, and then drops (frame-advance rolls) balls to advance to the next frame. Unlike the previous score-spare, these new helper functions cleanly express domain knowledge.

(defn balls-to-score
  "How many balls contribute to this frame's score?"
  [rolls]
  (cond
   (strike? rolls) 3
   (spare? rolls) 3
   :else 2))

Helper functions like strike and spare are also simple expressions of domain knowledge:

(defn strike? [rolls]
  (= 10 (first rolls)))

Keeping score

With the toolkit of frames and its various helpers in place, it is simple to score an entire game. Just convert rolls to frames, score the frames, and sum the score. The function reads like a definition, once you are accustomed to the notions of map and reduce:

(defn score-game
  "Score a bowling game, passed as a sequence of rolls."
  [rolls]
  (reduce + (map score-frame (take 10 (frames rolls)))))    

Build a toolkit, not a tool

Because all of the functions that support score-game are individually valuable tools, it is easy to implement new domain concepts. If you wanted to get the individual frame scores of a game in progress:

(map score-frame (frames [10 10 10 10 10]))
-> (30 30 30 20 10)

Or the running score:

; uses clojure.contrib.seq-utils.reductions
(reductions + (map score-frame (frames [10 10 10 10 10])))
-> (30 60 90 110 120)

Or the number of strikes:

(count (filter strike? (frames [10 0 10 0 10])))
-> 1    ; think about it

Parting shots

There is a lot going on here, but the single most important step is transforming sequences. Because the code now transforms sequences of balls to sequences of frames, it can be used to do all kinds of things other than just scoring the entire game. Moreover, the code is now shorter (in lines of code) and also more readable. Ditto the tests.

If you can think of some way to decompose a problem, TDD will help you build the individual pieces. But TDD does not help you choose the best decomposition. Functional decomposition is generally superior to object-oriented decomposition using mutable state.

Note that the use of laziness in the scoring is slightly gratuitous in this example, since bowling games are of a fixed size. However, laziness is a very useful part of solving real-world sized problems, and it is almost as easy to write as non-lazy code. So I tend to write laziness-preserving transformations even when I know (famous last words) that domain entities will never get large.

Resources

Comments (View)
Page 1 of 1
blog comments powered by Disqus