Uncle Bob, JSPS: Learing Clojure
Posted by Uncle Bob on 07/19/2009
(JSPS: Just Some Poor Schmuck)
I’m trying to learn Clojure, and I’m finding the effort challenging. Perhaps you can help me.
Programming in Clojure (A derivative of Lisp) requires a certain “inside-out” thinking. I’m finding the combination of the shift in thought process and TDD quite difficult to resolve. So I thought I’d ask for your help.
Below is the Bowling Game written in Clojure complete with unit tests. I’m not at all pleased with the result. The tests, especially, look bizzare and backwards to me.
Please post your own solutions to this so that we can all compare and contrast them.
Tests
(ns bowling-game) (use 'clojure.contrib.test-is) (use 'bowling-game) (defn roll-list [game list] (if (empty? list) game (roll-list (roll game (first list)) (rest list)) )) (defn roll-many [game n pins] (loop [i n g game] (if (zero? i) g (recur (dec i) (roll g pins))))) (defn gutter-game [game] (roll-many game 20 0)) (deftest can-create-game (is (not (nil? (new-game))))) (deftest gutter-game-should-score-0 (is (= 0 (score (gutter-game (new-game)))))) (deftest all-ones-should-score-20 (is (= 20 (score (roll-many (new-game) 20 1))))) (deftest one-spare (is (= 16 (score (roll-many (roll-list (new-game) [5 5 3]) 17 0))))) (deftest one_strike (is (= 24 (score (roll-many (roll-list (new-game) [10 3 4]) 16 0))))) (deftest perfect-game (is (= 300 (score (roll-many (new-game) 12 10))))) (run-tests 'bowling-game)
Code
(ns bowling-game) (defn new-game [] []) (defn next-two-balls [rolls] (+ (rolls 0) (rolls 1))) (defn score-strike [rolls] [1 (+ 10 (+ (rolls 1) (rolls 2)))]) (defn score-spare [rolls] [2 (+ 10 (rolls 2))]) (defn score-no-mark [rolls] [2 (next-two-balls rolls)]) (defn score-next-frame [rolls] (if (= 10 (first rolls)) (score-strike rolls) (if (= 10 (next-two-balls rolls)) (score-spare rolls) (score-no-mark rolls)))) (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))))))) (defn roll [game pins] (conj game pins))
Comments
Jason about 2 hours later:
I think it would probably be more beneficial to focus on clojure tha language and not get hung up on the testing aspect of it at first. You may hate this advise but it will ultimately make more sense in the long run. Functional programming is different and you shouldfocus on that. Testing will follow and make much more sense in time.
Auramo about 4 hours later:
One thing you could do is use the let construct more in the tests to name your return values you are asserting against.
Auramo about 6 hours later:
After thinking about my previous comment for a while I realize that my advice on let would just introduce clutter unless you extract the actual contents of the original tests to a separate function. Doing that might be overkill.
The test-is library IMHO would benefit from xUnit-like functions/macros: is-equal, is-not etc. These are of course trivial to write, but it’s always better to have them in the framework. I quess test-is doesn’t have those because it tries to be as minimal as possible (?)
Chris about 6 hours later:
I am also having a bit of a challenge w/ Clojure. I will say that the prag prog Programming Clojure book has been a useful assistant. I have not been able to vocalize specific challenge with it but I believe ‘inside out thinking’ is pretty accurate.
Test first is simply the way I think and I perhaps have a more challenging time expressing the idealized API of the code under test. This could simply be a language newb issue, or it could be something else. It could easily be that I can’t really see the cross programming paradigm fp/oo first principles…
I think that is likely it; I am having trouble sorting out is precisely what ‘first principles’ are applicable in non-oo languages. These principles are even more difficult to pull out in hybridized oo/ fp languages. In a way I have had an easier time in Haskell than in Scala, F# and Clojure.
DRY, small, intention revealing, factored – more cross paradigm rules???
Michael Harrison about 11 hours later:
Bob, it’s great to hear you’re interested in Clojure! I hope you enjoy your journey.
That “inside-out” feeling you’re having is probably the cause of your unease about the tests you’re writing. Clojure, like Lisps in general, is more about the application of functions to the results of other applications of functions: (roll-list (roll-many (new game).... You may find that in time you get used to these nested parenthetical function applications, and it becomes much faster to parse them mentally. But here are some thoughts on making the test definitions less bizarre.
You’re doing what I would do in your tests, creating helpful functions like roll-list, roll-many, and gutter-game so actual deftest expressions are as terse as possible.
You could experiment with doing more “setup” work in the defns, or even just making them value definitions:
(def gutter-game-score (score (roll-many (new-game) 20 0)))
(deftest gutter-game-should-score-0
(is (= 0 gutter-game-score)))
This might be appropriate if you can’t think of tests that require you to pass an argument other than a new game, and if you’re only focused on scores produced by your code. I think (= 0 gutter-game-score) is a very tidy assertion.
As Auramo suggested, you can use let to define values in your tests, to make the actual assertion terser:
(deftest one-spare
(let [one-spare-score (score (roll-many
(roll-list (new-game) [5 5 3]) 17 0))]
(is (= 16 one-spare-score))))
It’s also likely that test frameworks in Clojure will improve and develop: it’s still early in the game.
One other tip for you. I noticed you named ‘one-spare’ with a hyphen and ‘one_strike’ with an underscore. Both are legal names, but you’ll save yourself some trouble deciding on one standard. I mixed the two symbols liberally early in my FP days, and misery briefly ensued as I confronted errors about missing functions.
Happy coding!
http://twitter.com/arwagner about 12 hours later:
Uncle Bob, I would just make two comments: one, the first two functions in your test code look like classic reduce and map functions, respectively. E.g., something roughly like this: (defn roll-list [game list] (reduce roll (conj game list))) and (defn roll-many [game n pins] (nth (iterate #(roll % pins) game) n))
Dave about 12 hours later:
Have you considered using Clojure’s -> macro? It lets you sequence together calls in a way which lets you get rid of the deep, back-to-front function call nesting.
Eg:
(deftest one-spare (is (= 16 (-> (new-game) (roll-list [5 5 3]) (roll-many 17 0) score))))
Micah Martin about 13 hours later:
A couple guys at 8th Light, including myself, went through this same exercise. My solution is posted athttp://blog.8thlight.com/articles/2009/7/20/bowling-in-clojure
I look forward to learning ways to write better clojure.
Kjetil Valstadsve about 13 hours later:
Non-OO programming can get “inside-out” if you’re too deeply embedded in OO thinking. Your first warning sign is learning Smalltalk.
As for first principles, look for the ground rules for ADT’s. In traditional message-sending-model OO such as Smalltalk and Java, ADT’s are types in hierarchies. They get decorated with methods and their instances send each other messages to get stuff done. In Scheme or Lisp, you define some list structure, and create the ADT by writing the functions that interpret it.
Then there is the multiple-dispatch we find in CLOS, and that we find in Clojure too. Message-sending OO is single-dispatch, meaning that each message has a single receiver, used to dispatch to actual methods. Multiple-dispatch means the method is resolved based on more instances. This step from single to multiple basically inverts the whole way you build abstractions. Instead of just thinking about your instances in a hierarchy, you must think of your methods in a hierarchy as well, and it runs off into multiple dimensions very quickly. This means Lispers have infinitely more potential for messing up, but I think they like to stick to the Lisp/Scheme way outlined above.
It will be interesting to see which best practices surface in the Clojure community over the next few years, it should be a common ground for many different schools of thought.
Stuart Halloway about 13 hours later:
Hi Uncle Bob, glad to see you are looking at Clojure. I am working on my solution over at http://github.com/stuarthalloway/clojure-bowling, and I will post in more detail when I am happy with it.
There’s already some pretty interesting stuff there—most importantly, thinking in terms of a sequence of scores instead of just a total.
Jim Oly about 14 hours later:
First, I agree with Michael Harrison that the test functions look better if you define values to test and then use them in the test function. I would likely do it this way:
This can be done with the other game tests as well, and makes it easier if you want to test different functions over the same game.
roll-many would benefit from the built-in repeat or replicate functions:
(defn (defn roll-many [game n pins] (roll-list game (replicate n pins)))
reduce was mentioned earlier as a way to build up a result from a collection. I used it to simplify roll-list, since each call to roll takes a game and a move to make a new game. It’s much nicer than a manual loop and recur when applicable.
(defn roll-list [game list] (reduce roll game list))
In fact, I would be tempted to add this functionality into new-game. The zero argument case would return the empty vector as normal, and the single argument case would act as roll-list on a new game:
(declare roll) (defn new-game ([] []) ([rs] (reduce roll [] rs)))
This would subsume some roll-list calls, though roll-list would still be needed for defining the single spare and strike games. You could create the game from any sequence, so both (new-game '(1 2 3)) and (new-game [1 2 3]) would work.
(Note: the initial declare is needed since roll is defined later. Moving rollearlier would eliminate the need, but having new-game precede it seems nicer.)
I’m still working on the main body of the code, but here are a few other changes I like. In score, instead of using let to hold the result of a score-next-frame call and pick out the pieces later, we can use destructuring to directly pull out and name them. frame-score becomes [rolls-used single-score], and we can use rolls-used and single-score instead of (frame-score 0), etc. Here’s my resulting score:
(defn score [game] (loop [frame 1 rolls game score 0] (if (> frame 10) score (let [[rolls-used single-score] (score-next-frame rolls)] (recur (inc frame) (subvec rolls rolls-used) (+ score single-score))))))
And lastly, I like the style of using cond for multiple condition statements, so I replaced the nested ifs in score-next-frame. I think this is up to the individual programmer, but having all the cases lined up looks nicer to me.
(defn score-next-frame [rolls] (cond (= 10 (first rolls)) (score-strike rolls) (= 10 (next-two-balls rolls)) (score-spare rolls) :else (score-no-mark rolls)))
Stuart Halloway about 14 hours later:
Ok, I am up to a solution I like reasonably well athttp://github.com/stuarthalloway/clojure-bowling. Here’s the key bits:
(def gutter-game (roll-many (new-game) 20 0)) (deftest gutter-game-should-score-0 (is (= 0 (score gutter-game))))
(ns bowling-game (:use clojure.contrib.seq-utils)) (defn strike? [rolls] (= 10 (first rolls))) (defn spare? [rolls] (= 10 (apply + (take 2 rolls)))) (defn balls-to-score "How many balls contribute to this frame's score?" [rolls] (cond (strike? rolls) 3 (spare? rolls) 3 :else 2)) (defn frame-advance "How many rolls should be consumed to advance to the next frame?" [rolls] (if (strike? rolls) 1 2)) (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)))))) (defn score "Score a bowling game, passed as a sequence of rolls." [rolls] (apply + (flatten (take 10 (frames rolls))))) (ns test.bowling-game (:use clojure.test) (:use bowling-game)) ; Tests could be improved by a macro that captures the descriptions, ; or by modifying 'are' to do so in some way. (deftest test-balls-to-score (are [description balls frames] (= balls (balls-to-score frames)) "strike" 3 [10 10 10] "spare" 3 [5 5 10] "no mark" 2 [5 3 5])) (deftest test-frame-advance (are [description advance frames] (= advance (frame-advance frames)) "strike" 1 [10] "spare" 2 [5 5] "no mark" 2 [5 4])) (deftest test-frames-for-various-games (are [description expected-frames game] (= expected-frames (take 10 (frames game))) "gutter game" (repeat 10 [0 0]) (repeat 0) "all ones" (repeat 10 [1 1]) (repeat 1) "all fives (spares)" (repeat 10 [5 5 5]) (repeat 5) "all tens (strikes)" (repeat 10 [10 10 10]) (repeat 10) )) (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) ))
Phil about 15 hours later:
Any chance you could syntax-highlight your code? My brain simply shuts down when I try to read monochrome code.
Jim Oly about 15 hours later:
I like Stuart’s approach. I was keeping the game in a vector as the original code did because it most easily supported the requirements for the roll function. It was causing problems in my attempts at simplifying the main body of code though. Switching over to sequences makes a lot of sense for score.
Out of curiosity, what is the idiomatic way of adding a single element to the end of a sequence? That would make it easy to implement roll and use sequences everywhere.
Mark Engelberg about 18 hours later:
Just use recursion, and the code writes itself…
(use 'clojure.contrib.test-is) (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 doinganything at the expense of clarity for a problem where the inputs are so small that efficiency is a non-issue.
Mark Engelberg about 18 hours later:
BTW, the best book for learning how to think recursively is How To Design Programs, available for free at htdp.org.
Mark Engelberg about 19 hours later:
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 a bit more compact without as many helper functions.
(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 about 19 hours later:
Sorry for the confusing choice of variable name. Should be “game” not “games” in:
(defn score [game] (sum (take 10 (frame-scores game))))
Stuart Halloway about 24 hours later:
While Mark’s solution is cool (and very short) I believe that decomposing into named helper functions better blends TDD and functional style. I have written up my approach over on the RunCodeRun blog.
Mark Triggs 1 day later:
Another variation on Stuart’s approach here, but modifying slightly to make the notion of “types of rolls” into a first-class concept:
(ns bowling-game (:use clojure.contrib.test-is clojure.contrib.seq-utils)) (defn sum [xs] (apply + xs)) (def *roll-types* [{:name "regular (underachieving?)" :satisfies #(< (sum (take 2 %)) 10) :consumes 2 :advances 2} {:name "strike" :satisfies #(= (first %) 10) :consumes 3 :advances 1} {:name "spare" :satisfies #(= (sum (take 2 %)) 10) :consumes 3 :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] (sum (map sum (take 10 (frames rolls))))) ;; tests (deftest test-score-game (is (= (score-game [1 5 3 6 7 2 3 6 4 4 5 3 3 3 4 5 8 1 2 6]) 81))) (deftest test-score-strike-game (is (= (score-game [10 3 6 7 2 3 6 4 4 5 3 3 3 4 5 8 1 2 6]) 94))) (deftest test-score-spare-game (is (= (score-game [1 9 3 6 7 2 3 6 4 4 5 3 3 3 4 5 8 1 2 6]) 88))) (deftest test-strike-then-spare (is (= (score-game [10 4 6 7 2 3 6 4 4 5 3 3 3 4 5 8 1 2 6]) 103))) (deftest test-two-strikes (is (= (score-game [10 10 7 2 3 6 4 4 5 3 3 3 4 5 8 1 2 6]) 112))) (deftest test-two-spares (is (= (score-game [8 2 5 5 7 2 3 6 4 4 5 3 3 3 4 5 8 1 2 6]) 98))) (deftest test-last-spare (is (= (score-game [1 5 3 6 7 2 3 6 4 4 5 3 3 3 4 5 8 1 2 8 7]) 90))) (deftest test-last-strike (is (= (score-game [1 5 3 6 7 2 3 6 4 4 5 3 3 3 4 5 8 1 10 7 2]) 92))) (deftest test-last-spare-then-strike (is (= (score-game [1 5 3 6 7 2 3 6 4 4 5 3 3 3 4 5 8 1 2 8 10]) 93))) (deftest test-perfect-game (is (= (score-game [10 10 10 10 10 10 10 10 10 10 10 10]) 300))) (deftest test-whole-game (is (= (score-game [6 3 7 1 8 2 7 2 10 6 2 7 3 10 8 0 7 3 10]) 135)))
Dan Pierkowski 1 day later:
A caveat:The guy I worked with on this and I are from Boston, so we scored this according to candle-pin rules. That may make this less useful for some folks.
A buddy and I were talking through designs for this, and as we talked about scoring frames, we realized that there were three cases:
1) Strike – score the first ball, plus the next two ‘bonus’ balls, then remove the first ball (the strike) from the list, then continue scoring.
2) Spare – score the first two balls, plus the next ‘bonus’ ball, then remove the first two balls (the spare) from the list, then continue scoring.
3) No-mark – score all three balls in the frame, then remove them from the list, then continue scoring. (Remember: candlepin!)
In all these cases you want to score three balls, the only difference was in how many balls you remove from the list before you continue scoring (i.e., how many balls were in the frame you are removing from the yet-to-be-scored list). So this is what we came up with:
(ns scorecard) (defn third [rolls] (first (next (next rolls)))) (defn first-two [rolls] (+ (first rolls) (second rolls))) (defn strike? [rolls] (= 10 (first rolls))) (defn spare? [rolls] (= 10 (first-two rolls))) (defn remove-strike [rolls] (rest rolls)) (defn remove-spare [rolls] (rest (rest rolls))) (defn remove-normal-frame [rolls] (rest (rest (rest rolls)))) (defn remove-frame [rolls] (if (strike? rolls) (remove-strike rolls) (if (spare? rolls) (remove-spare rolls) (remove-normal-frame rolls)))) (defn score [input-rolls] (loop [rolls input-rolls score 0 frame-counter 0] (if (or (empty? rolls) (= 10 frame-counter)) score (recur (remove-frame rolls) (+ score (first rolls) (second rolls) (third rolls)) (inc frame-counter)))))
And the tests:
(ns scorecard-test (:use clojure.contrib.test-is scorecard)) (deftest test-third (is (= 3 (third [1 2 3 4])))) (deftest test-first-two (is (= 3 (first-two [1 2 3])))) (deftest test-remove-frame (is (= [3 4 5] (remove-frame [0 1 2 3 4 5]))) (is (= [3 4] (remove-frame [10 3 4]))) (is (= [5] (remove-frame [6 4 5])))) (deftest test-remvo-two-frames (is (= [] (remove-frame (remove-frame [0 1 2 0 0 0]))))) (deftest test-scores (is (= 0 (score []))) (is (= 6 (score [1 2 3]))) (is (= 15 (score [1 2 3 4 5 0]))) (is (= 19 (score [10 1 2 3]))) (is (= 17 (score [5 5 1 2 3]))) (is (= 300 (score [10 10 10 10 10 10 10 10 10 10 10 10]))) (is (= 19 (score [5 5 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 ]))) (is (= 21 (score [10 1 1 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 ]))))
Uncle Roastie 4 days later:
Uncle Bob, we met several times at Xerox – you taught me the real intent of OOD/OOP. I’ve since moved on to C#, and some guys I work with decided to take a functional language course last fall to get mentally into modern days. The end result: you must think bottom-up for FP – get rid of the top-down mindset (for FP). Small-small functions (the mechanism code), no nested ifs, and your higher level functions (the policy code) just call the lower-level ones. Needing some closures is common. Map your tests to the functions after you are done. Once you are comfortable with the mindset, then you can start writing your tests first. Get comfortable with map/filter/reduce.