More Clojure from Uncle Bob JSPS
Posted by Uncle Bob on 07/31/2009
Thanks to all who posted their solutions to the Bowling Game. Just to be contrary, I decided to implement yet another kind of solution. This one clearly shows my OO roots.
h2. Here are the tests.
(ns bowling-game) (use 'clojure.contrib.test-is) (use 'bowling-game) (defn roll-list [game list] (vec (concat game list))) (defn roll-many [game n pins] (roll-list game (repeat n pins))) (deftest can-create-game (is (not (nil? (new-game))))) (deftest gutter-game-should-score-0 (is (= 0 (score (-> (new-game) (roll-many 20 0)))))) (deftest all-ones-should-score-20 (is (= 20 (score (-> (new-game) (roll-many 20 1)))))) (deftest one-spare (is (= 16 (score (-> (new-game) (roll-list [5 5 3]) (roll-many 17 0)))))) (deftest one_strike (is (= 24 (score (-> (new-game) (roll-list [10 3 4]) (roll-many 16 0)))))) (deftest perfect-game (is (= 300 (score (-> (new-game) (roll-many 12 10)))))) (run-tests 'bowling-game)
And here is the code.
(ns bowling-game) (defn new-game [] []) (defn frame-type [[first second]] (cond (= 10 first) :strike (= 10 (+ first second)) :spare :else :no-mark)) (defmulti frame-size frame-type) (defmethod frame-size :strike [_] 1) (defmethod frame-size :spare [_] 2) (defmethod frame-size :no-mark [_] 2) (defmulti frame-score frame-type) (defmethod frame-score :strike [[_ first second]] (+ 10 (+ first second))) (defmethod frame-score :spare [[_ _ next]] (+ 10 next)) (defmethod frame-score :no-mark [[first second]] (+ first second)) (defn score [game] (loop [frame 1 rolls game score 0] (if (> frame 10) score (recur (inc frame) (subvec rolls (frame-size rolls)) (+ score (frame-score rolls))))))
Comments
http://uglylispcode.wordpress.com 1 day later:
@uncle bob,
If any yahoos try telling you that functional programming is not OOP, they are blowing smoke.
A lambda over let over lambda is an object.
To me the most important part of OOP is data encapsulation and having the methods that transform the data into the behavior that the object needs and the above definitely qualifies.
You don’t think that the principles that you use for OOP no longer apply when working with a functional program? OCP, SRP, LSP, and all the rest definitely apply.
If you keep in mind that sequences (alist and plist) is the basic data types. And that the operators (I say operators not methods) are used to transform (iteration and recursion) the lists into the outputs you desire.
When I started working with CL, I started with making enhancements to a unit test framework called lisp-unit. What I wanted is to change its output and also to add fixtures to my tests.
For me the unit test framework is ugly: (is(= 0 0)) what the hell! The first thing I would do is change it something like this: (should= 0, 0) or (is= 0, 0). The parenthesis has too much noise. I understand why it is, but still the nice thing about using a lisp language is that you can change it to go in the direction of a DSL. Unit testing is an DSL.
I highly recommend that when starting a new language use it with something that you do know
Right now I’ve stopped with the CL and am back into the dotnet world. I want to bring some of the things I learned with CL back into C#.
My two blogs are here, check them out: http://uglylispcode.wordpress.comhttp://uglycode.wordpress.com
Philip Schwarz 1 day later:
@Uncle Bob
When I saw your previous blog entry, Uncle Bob, JSPS: Learning Clojure, I had a go at coding the bowling game, but didn’t get round to posting my solution because I got interrupted by something else.
I had gone for exactly the same frame-type function as you did! Here is my attempt (my first Clojure program):
(defn frame-type [[first second]] (cond (= 10 first) :strike (= 10 (+ first second)) :spare :else :normal)) (defmulti score frame-type) (defmethod score :strike [[_ first second & rest]] (+ 10 first second (if (nil? rest) 0 (score (into [first second] rest))))) (defmethod score :spare [[_ _ next & rest]] (+ 10 next (if (nil? rest) 0 (score (cons next rest))))) (defmethod score :normal [[first second & rest]] (+ first second (if (nil? rest) 0 (score rest))))
At the time, I did not write it test-first, but I have now tested it using a minor adaptation of your tests:
(deftest gutter-game-should-score-0 (is (= 0 (score (repeat 20 0))))) (deftest all-ones-should-score-20 (is (= 20 (score (repeat 20 1))))) (deftest one-spare (is (= 16 (score (into [5 5 3] (repeat 17 0) ) )))) (deftest one-strike (is (= 24 (score (into [10 3 4] (repeat 16 0)))))) (deftest perfect-game (is (= 300 (score (repeat 12 10))))) (run-tests)
I prefer my Haskell version of the Bowling Game:
score [x, y] = x + y -- Normal Frame score [10, x, y] = 10 + x + y -- Strike score [x, y, z] = 10 + z -- Spare score (10:(x:(y:rest))) = 10 + x + y + score (x:(y:rest)) -- Strike score (x:(y:(z:rest))) | (x + y) == 10 = 10 + z + score (z:rest) -- Spare score (x:(y:rest)) | otherwise = x + y + score rest -- Normal Frame
It is easier to read/understand.
Your initial tweet about the Clojure book prompted me to get a copy of the book. I agree with your later tweet, that the book is disappointing because it feels rushed, but I guess that I’ll persevere with it, especially since you lately tweeted that your opinion of the book is changing, that the organization is frustrating, but the content is rich.
I look forward to your future thoughts on functional programming.