I've been enjoying my journey down the path of functional languages. I started with Clojure, than looked at Erlang, and now I am back at Clojure again. This post does not try to explain why you should look at functional languages, I assume you passed that since you're reading this blog post.
The books will teach you the basic concepts. Those ideas vanish fast unless I practice them with a project or some kind of challenge. Programming quizzes or coding puzzles are a good way to exercise a new language. I usually start with the simple ones, and I work my way towards the more complex projects. The FizzBuzz kata is on the easier side, I was even asked to code it on a white board not too long ago.
Here is how I solved the FizzBuzz kata in Clojure test driving it with its own test-is tool. I describe every changes I made, feel free to follow along by typing in the way I did it.
I'll assume you have Clojure and Leiningen installed on your computer. Getting these tools is simple thanks to Homebrew on OSX. Please do a Google search if you don't have them already installed and you're using an OS other than OSX. I am using Clojure 1.5.1 version at the time of this writing.
I generated a skeleton project with Leiningen:lein new app fizzbuzz. The directory structure had a src and a test directory. lein test runs the tests and lein run executes the program.
I put all my code in the src/fizzbuzz/core.clj and in the test/fizzbuzz/core_test.clj files. I write code in Vim, and I use a vertical split to look at the test and program files in the same window. I usually have the test on the left, and the code under test on the right hand side.
Here is the first failing test I wrote:
(ns fizzbuzz.core-test
(:require [clojure.test :refer :all]
[fizzbuzz.core :refer :all]))
(deftest fizz-buzz-printer-test
(testing "with 1"
(is (= "1\n" (fizz-buzz-printer 1)))))
I provided the skeleton for the fizz-buzz-printer function this way:
(ns fizzbuzz.core (:gen-class)) (defn fizz-buzz-printer [limit]) (defn -main "I don't do a whole lot ... yet." [& args] ;; work around dangerous default behaviour in Clojure (alter-var-root #'*read-eval* (constantly false)) (println "Hello, World!"))When I ran the test with
lein test, this is the error I had:
FAIL in (fizz-buzz-printer-test) (core_test.clj:7)
with 1
expected: (= "1\n" (fizz-buzz-printer 1))
actual:
(not (= 1
nil))
Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
Tests failed.
The simplest thing that could possibly work is this:
(ns fizzbuzz.core (:gen-class)) (defn fizz-buzz-printer [limit] "1\n") ...Please note that I omitted the
-main function as it's not changed yet. I ran the tests, and it all passed:
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
Great! Now I am on to the next test:
...
(deftest fizz-buzz-printer-test
(testing "with 1"
(is (= "1\n" (fizz-buzz-printer 1))))
(testing "with 2"
(is (= "1\n2\n" (fizz-buzz-printer 2)))))
The following code change made the test pass:
...
(defn fizz-buzz-printer [limit]
(if (= 2 limit)
"1\n2\n"
"1\n"))
...
A conditional won't serve me long to print out numbers. I refactored the function this way:
...
(defn fizz-buzz-printer
([output current limit]
(let [product (str output current "\n")]
(if (< current limit)
(fizz-buzz-printer product (+ 1 current) limit)
product)))
([limit]
(fizz-buzz-printer "" 1 limit)))
...
This might need a little explanation. I chose recursion instead of using a loop. The fizz-buzz-printer function is overloaded: it accepts 1 (the limit) or 3 arguments (the output, current value and limit). The formatted string is captured in the product value. If the current value in the cycle is less than the limit I call the same function but the current value is incremented by one, and if not, I know I reached the limit and just return the formatted string - the product.
With this code I should be able to print the numbers on the screen followed by a line break. I wrote a quick - temporary - test to verify that (see the last assert):
...
(deftest fizz-buzz-printer-test
(testing "with 1"
(is (= "1\n" (fizz-buzz-printer 1))))
(testing "with 2"
(is (= "1\n2\n" (fizz-buzz-printer 2))))
(testing "with 4"
(is (= "1\n2\n3\n4\n" (fizz-buzz-printer 4)))))
I was ready for the fun part of this kata! For every number divisible by 3 I had to print out the word: "Fizz".
First I wrote the failing test:
...
(deftest fizz-buzz-printer-test
(testing "with 1"
(is (= "1\n" (fizz-buzz-printer 1))))
(testing "with 2"
(is (= "1\n2\n" (fizz-buzz-printer 2))))
(testing "from 1-3"
(is (= "1\n2\nFizz\n" (fizz-buzz-printer 3)))))
The error was obvious:
FAIL in (fizz-buzz-printer-test) (core_test.clj:11)
from 1-3
expected: (= "1\n2\nFizz\n" (fizz-buzz-printer 3))
actual: (not (= "1\n2\nFizz\n" "1\n2\n3\n"))
Ran 1 tests containing 3 assertions.
1 failures, 0 errors.
Tests failed.
I was expecting the word Fizz, but I was getting the number 3. That logic was missing. Here is how I added that:
...
(defn fizz-buzz-printer
([output current limit]
(let [product (if (= 3 current)
(str output "Fizz\n")
(str output current "\n"))]
(if (< current limit)
(fizz-buzz-printer product (+ 1 current) limit)
product)))
([limit]
(fizz-buzz-printer "" 1 limit)))
...
I felt generating the formatted string - the product value - put way too much logic in the let function. I extracted out that logic into its own function called format-output:
...
(defn- format-output [output current]
(if (= 3 current)
(str output "Fizz\n")
(str output current "\n")))
(defn fizz-buzz-printer
([output current limit]
(let [product (format-output output current)]
(if (< current limit)
(fizz-buzz-printer product (+ 1 current) limit)
product)))
([limit]
(fizz-buzz-printer "" 1 limit)))
...
The function format-output is private, it's only visible within its own namespace thanks to declaring it with defn-.The increased complexity in the let function triggered the "extract function" refactoring. I felt the
format-output function should be responsible for deciding if it has to print the number or the words "Fizz", "Buzz" or "FizzBuzz".
The next test was simple, I wanted to make sure the logic worked from zero to four. I did not have to modify the code for it:
...
(deftest fizz-buzz-printer-test
(testing "with 1"
(is (= "1\n" (fizz-buzz-printer 1))))
(testing "with 2"
(is (= "1\n2\n" (fizz-buzz-printer 2))))
(testing "from 1-3"
(is (= "1\n2\nFizz\n" (fizz-buzz-printer 3))))
(testing "from 1-4"
(is (= "1\n2\nFizz\n4\n" (fizz-buzz-printer 4)))))
I reached a point with my tests where I had to make my code a bit easier to test. I did not want to write tests that compares more than 3 numbers from the fizz-buzz-printer. I felt 3 is a large enough set to verify the boundaries. I added a new test for this:
...
(deftest fizz-buzz-printer-test
(testing "with 1"
(is (= "1\n" (fizz-buzz-printer 1))))
(testing "with 2"
(is (= "1\n2\n" (fizz-buzz-printer 2))))
(testing "from 1-3"
(is (= "1\n2\nFizz\n" (fizz-buzz-printer 3))))
(testing "from 1-4"
(is (= "1\n2\nFizz\n4\n" (fizz-buzz-printer 4))))
(testing "from 3-4"
(is (= "Fizz\n4\n" (fizz-buzz-printer 3 4)))))
Since I did not have the function override for 2 arguments, the test failed:
ERROR in (fizz-buzz-printer-test) (AFn.java:437)
from 3-4
expected: (= "Fizz\n4\n" (fizz-buzz-printer 3 4))
actual: clojure.lang.ArityException: Wrong number of args (2) passed to: core$fizz-buzz-printer
I added the function override which looked almost identical to the function with one argument:
...
(defn fizz-buzz-printer
([output current limit]
(let [product (format-output output current)]
(if (< current limit)
(fizz-buzz-printer product (+ 1 current) limit)
product)))
([start limit]
(fizz-buzz-printer "" start limit)) ; added function with 2 arguments
([limit]
(fizz-buzz-printer "" 1 limit)))
...
All the tests passed. However, this code had duplication: the unary function (method with one argument) initialized the output just like the binary function. I decided to remove this duplication by changing the unary function to call the binary function that calls the ternary one.
...
(defn fizz-buzz-printer
([output current limit]
(let [product (format-output output current)]
(if (< current limit)
(fizz-buzz-printer product (+ 1 current) limit)
product)))
([start limit]
(fizz-buzz-printer "" start limit))
([limit]
(fizz-buzz-printer 1 limit))) ; does not initializes the output
...
I was now ready to zoom into the ranges where the program had to produce the different words instead of numbers. The next test did just that:
...
(deftest fizz-buzz-printer-test
(testing "with 1"
(is (= "1\n" (fizz-buzz-printer 1))))
(testing "with 2"
(is (= "1\n2\n" (fizz-buzz-printer 2))))
(testing "from 1-3"
(is (= "1\n2\nFizz\n" (fizz-buzz-printer 3))))
(testing "from 1-4"
(is (= "1\n2\nFizz\n4\n" (fizz-buzz-printer 4))))
(testing "from 3-4"
(is (= "Fizz\n4\n" (fizz-buzz-printer 3 4))))
(testing "from 4-5"
(is (= "4\nBuzz\n" (fizz-buzz-printer 4 5)))))
I did the simplest thing that could possibly work: I used a nested conditional:
...
(defn- format-output [output current]
(if (= 3 current)
(str output "Fizz\n")
(if (= 5 current) ; introduced this nested conditional
(str output "Buzz\n")
(str output current "\n"))))
...
Everything passed again, but this code was ugly. Here is how I used cond to refactor it:
...
(defn- format-output [output current]
(cond
(= 3 current) (str output "Fizz\n")
(= 5 current) (str output "Buzz\n")
:else (str output current "\n")))
...
My next test covered the range from 4-6, where the word "Fizz" should be printed out for the number 6.
...
(deftest fizz-buzz-printer-test
(testing "with 1"
(is (= "1\n" (fizz-buzz-printer 1))))
(testing "with 2"
(is (= "1\n2\n" (fizz-buzz-printer 2))))
(testing "from 1-3"
(is (= "1\n2\nFizz\n" (fizz-buzz-printer 3))))
(testing "from 1-4"
(is (= "1\n2\nFizz\n4\n" (fizz-buzz-printer 4))))
(testing "from 3-4"
(is (= "Fizz\n4\n" (fizz-buzz-printer 3 4))))
(testing "from 4-5"
(is (= "4\nBuzz\n" (fizz-buzz-printer 4 5))))
(testing "from 4-6"
(is (= "4\nBuzz\nFizz\n" (fizz-buzz-printer 4 6)))))
This test failed again as I was only checking for the number three and not for numbers divisible by three without a remainder. I changed the format-output function to use the modulus operator:
...
(defn- format-output [output current]
(cond
(= 0 (mod current 3)) (str output "Fizz\n")
(= 5 current) (str output "Buzz\n")
:else (str output current "\n")))
...
All the tests now passed again.
I suspected the same error at number 10, therefore my next test was around it (between 9 and 11):
...
(deftest fizz-buzz-printer-test
(testing "with 1"
(is (= "1\n" (fizz-buzz-printer 1))))
(testing "with 2"
(is (= "1\n2\n" (fizz-buzz-printer 2))))
(testing "from 1-3"
(is (= "1\n2\nFizz\n" (fizz-buzz-printer 3))))
(testing "from 1-4"
(is (= "1\n2\nFizz\n4\n" (fizz-buzz-printer 4))))
(testing "from 3-4"
(is (= "Fizz\n4\n" (fizz-buzz-printer 3 4))))
(testing "from 4-5"
(is (= "4\nBuzz\n" (fizz-buzz-printer 4 5))))
(testing "from 4-6"
(is (= "4\nBuzz\nFizz\n" (fizz-buzz-printer 4 6))))
(testing "from 9-11"
(is (= "Fizz\nBuzz\n11\n" (fizz-buzz-printer 9 11)))))
The test failed, I received 10 instead of "Buzz", I adjusted the second conditional to use the modulus function as well:
...
(defn- format-output [output current]
(cond
(= 0 (mod current 3)) (str output "Fizz\n")
(= 0 (mod current 5)) (str output "Buzz\n")
:else (str output current "\n")))
...
This was great! Now I had to write code for one more scenario: for the numbers divisible by 5 and 3 I had to print the word "FizzBuzz" instead of "Fizz" or "Buzz". My last test did just that:
...
(deftest fizz-buzz-printer-test
(testing "with 1"
(is (= "1\n" (fizz-buzz-printer 1))))
(testing "with 2"
(is (= "1\n2\n" (fizz-buzz-printer 2))))
(testing "from 1-3"
(is (= "1\n2\nFizz\n" (fizz-buzz-printer 3))))
(testing "from 1-4"
(is (= "1\n2\nFizz\n4\n" (fizz-buzz-printer 4))))
(testing "from 3-4"
(is (= "Fizz\n4\n" (fizz-buzz-printer 3 4))))
(testing "from 4-5"
(is (= "4\nBuzz\n" (fizz-buzz-printer 4 5))))
(testing "from 4-6"
(is (= "4\nBuzz\nFizz\n" (fizz-buzz-printer 4 6))))
(testing "from 9-11"
(is (= "Fizz\nBuzz\n11\n" (fizz-buzz-printer 9 11))))
(testing "from 14-16"
(is (= "14\nFizzBuzz\n16\n" (fizz-buzz-printer 14 16)))))
The error was what I expected: the program only printed out the word "Fizz" instead of "FizzBuzz". Adding the modulus check for 15 made this last test to pass:
...
(defn- format-output [output current]
(cond
(= 0 (mod current 15)) (str output "FizzBuzz\n")
(= 0 (mod current 3)) (str output "Fizz\n")
(= 0 (mod current 5)) (str output "Buzz\n")
:else (str output current "\n")))
...
All my tests passed!
Ran 1 tests containing 9 assertions.
0 failures, 0 errors.
I also wanted to see the program in action by running it with leain run. Here is how I changed the main function:
... (defn -main [& args] ;; work around dangerous default behaviour in Clojure (alter-var-root #'*read-eval* (constantly false)) (print (fizz-buzz-printer 100)))
And that's it! Please take a look at the final solution in this Gist.
Hope you had as much fun reading this as I had preparing it.
Happy New Year everyone!
