Diamond Kata using Clojure and TDD

After seeing Ron Jeffries' post about different takes on the Diamond Kata, I've decided to try it as well.

This comment on Philip Schwarz' solution in particular got my attention:

I don’t know Clojure, which certainly made Philip’s solution harder to grok, but one can sort of read it. He added some tests later, which certainly helps.

Would a solution driven by tests be easier to understand? I'll let you be the judge. Check out my solution on GitHub now, or keep reading for a detailed description (spoilers) of how I got to it.

My solution step-by-step

To avoid spoiling my take on it, I decided not to read other solutions beforehand. I used Seb Rose's initial article as a reference and tried to start with the simplest test I could think of:

(deftest diamond-building
  (testing "Diamond for A"
    (is (= '("A") (diamond "A")))))

Followed by a fake implementation:

(defn diamond [letter] '("A"))

So with my initial test passing I tried to jump to something a bit more meaningful like:

(testing "Diamond for B"
    (is (= '("-A-", "B-B", "-A-") (diamond "B"))))

And that was enough to get me stuck! I felt overwhelmed by the amount of complexity that the proper solution for the second test required and saw myself forced to step back and try to break the problem into smaller parts that I could fit into my head.

To decide how to split the problem, I've looked at another example, the diamond for C:

--A--
-B-B-
C---C
-B-B-
--A--

I noticed I could try to tackle a few parts of the problem independently:

  1. The amount of spaces outside the diamond for each letter
  2. The amount of spaces inside the diamond for each letter
  3. Putting a line of the diamond together
  4. The letter sequence to compose the rows diamond

I've decided to start with #2 because it looked simpler than #1: the inner part of the diamond does not depend on the size of the diamond itself (e.g. B-B and C---C will always have the same amount of spaces inside).

The inner part of the diamond

It also happens that the number of spaces inside the diamond follow a simple formula: (2 * n) - 1, where n is the index of the letter in the diamond. The only exception is the letter A that doesn't contain any space and is not repeated on its line.

So I've parked my initial tests and wrote a new one to help me implement just the inner part of a line:

(deftest inner-part-building  
  (testing "A"
    (is (= "A" (inner-part "A"))))

That was easy enough to fake as well:

(defn inner-part [letter] "A")

So again, I've added a few more test cases:

(deftest inner-part-building
  (testing "A"
    (is (= "A" (inner-part "A"))))
  (testing "B"
    (is (= "B-B" (inner-part "B")))))

Then I could start splitting the logic for A and the other letters by introducing a conditional:

(defn inner-part [letter]
  (cond (= "A" letter) letter
        :else "B-B")

At this point I decided to replace the B-B from my solution by the actual implementation.

So I added the test case for C and realised I still didn't have a function to give me the index of a particular letter (A -> 0, B -> 1, C -> 2 and so on) to use in the formula mentioned above.

After a few more red-green cycles (which I'll omit from now on as it gets boring really quickly), I've ended up with this two functions:

(defn char-index [letter] (- (int (first (char-array letter))) 65))

(defn inner-part [letter]
  (let [index (char-index letter)]
    (cond (= 0 index) letter
          :else (str letter (string/join "" (repeat (- (* 2 index) 1) "-")) letter))))

The char-index function calculates the index of the letter by converting it to int and using the difference from the int value of A which is 65.

The inner part is then created by concatenating the letter + ((2 * n) -1) dashes + letter.

The outer part of the diamond

To calculate the outer part of the diamond I started once more with the simplest case:

(deftest outer-part-building
  (testing "A for A diamond"
    (is (= "" (outer-part "A" "A"))))

In this case I've used two parameters: one to represent the letter of the line, another one to represent the letter of the diamond. Again, the first implementation was pretty much a stub:

(defn outer-part [current-letter diamond-letter] "")

Then I've moved to more test cases for the letter A:

(deftest outer-part-building
  (testing "A for A diamond"
    (is (= "" (outer-part "A" "A"))))
  (testing "A for B diamond"
    (is (= "-" (outer-part "A" "B"))))
  (testing "A for C diamond"
    (is (= "--" (outer-part "A" "C")))))

At this point I could tell that the number of spaces depended on the second parameter, so I've implemented it accordingly:

(defn outer-part [current-letter diamond-letter]
  (let [final-index (char-index diamond-letter)]
    (string/join "" (repeat final-index "-"))))

All the tests were passing but I was pretty sure it wouldn't work for other letters, so I've added another test to show I needed to use the formula index of diamond letter - index of current letter to figure out the correct number of spaces:

(testing "B for F diamond"
    (is (= "----" (outer-part "B" "F"))))

Surely enough it failed because my function was just using the diamond index.

By fixing that I've ended up with the final version of the function:

(defn outer-part [current-letter diamond-letter]
  (let [current-index (char-index current-letter)
        final-index (char-index diamond-letter)]
    (string/join "" (repeat (- final-index current-index) "-"))))

Creating the rows

With the basic parts of my solution implemented, now it was time to start putting the pieces together. I've started by the simplest case of single diamond line. The test ended up like:

(deftest line-building
  (testing "A for A diamond"
    (is (= "A" (line-for "A" "A"))))
  (testing "A for C diamond"
    (is (= "--A--" (line-for "A" "C"))))
  (testing "C for C diamond"
    (is (= "C---C" (line-for "C" "C")))))

And the implementation was very simple:

(defn line-for [current-letter diamond-letter]
  (let [outer (outer-part current-letter diamond-letter)
        inner (inner-part current-letter)]
    (str outer inner outer)))

The diamond letter sequence

Now the final big challenge was to create the sequence of the letters for the diamond. I wanted something to produce:

(deftest diamond-letters-building
  (testing "A diamond"
    (is (= '("A") (letter-sequence "A"))))
  (testing "D diamond"
    (is (= '("A" "B" "C" "D" "C" "B" "A") (letter-sequence "D")))))

Once again the easiest way I could find to achieve that was to use char -> int conversions, which allowed me to create the following function:

(defn letter-sequence [diamond-letter]
  (let [value-a 65
        value-letter (+ value-a (char-index diamond-letter))]
    (map #(str (char %)) (concat (range value-a value-letter) (range value-letter (- value-a 1) -1)))))

The result is created by:

  • Creating a first list of ints, from A up to one letter before the diamond letter ((range value-a value-letter) part)
  • Creating a second list from the diamond letter back to A ((range value-letter (- value-a 1) -1)))
  • Merge the two lists using concat
  • Use map to convert the ints back to strings.

Putting everything together

At this stage I could go back to my initial tests:

(deftest diamond-building
  (testing "Diamond for A"
    (is (= '("A") (diamond "A"))))
  (testing "Diamond for B"
    (is (= '("-A-", "B-B", "-A-") (diamond "B"))))
  (testing "Diamond for C"
    (is (= '("--A--", "-B-B-", "C---C", "-B-B-", "--A--") (diamond "C"))))

The implementation of the diamond function became trivial:

(defn diamond [letter]
  (map #(line-for % letter) (letter-sequence letter)))

And that was it. By mapping the line-for function to each of the letters produced by letter-sequence I could create the final structure of the diamond.

Conclusion

Using TDD forced me to realise early on that the problem was not as simple as I thought initially. Using output examples helped me to understand the problem a bit more and breaking it into smaller parts made the red-green-refactor flow very easy.

It was specially fun to see the problem become simpler and simpler as the parts of it were being implemented.

The final solution also became simpler than I expected. Comparing to Philip's solution it's also required half of his lines of code.

I can't decide if that was just luck on my approach or was a side-effect of TDD. It was definitely not because my Clojure skills, as I'm relatively new to the language.

Overall it was a very fun exercise. Now is time to go back and read how other people have done it.

Closing the Loop

As developers we should be experts in feedback: we want to learn about the consequences of our code changes as quickly as possible. For instance:

  • The editor can tell if the new syntax is correct.
  • Unit tests can tell if basic logic and interactions are as expected.
  • Integration and Acceptance tests can tell how the change will affect the rest of the system.
  • Continuous integration can tell if changes coming from different developers will work together.
  • Build pipeline can tell if the changed system can be deployed and run after each change.
  • Production monitoring can tell if the system is healthy and alert us otherwise.

Those are great techniques for building things right, but they don't tell if we are building the right things.

For that we should be trying harder to close the feedback loop of Planning, Building, Measuring and Learning. So far I've noticed the following approaches have the tendency to help:

  • Make sure requirements are created from meaningful conversations (Impact Mapping)
  • Include relevant data gathering into each feature (How to Measure Anything) and make business key performance indicators (KPI) part of the system monitoring
  • Use Retrospectives to reflect on the data collected and general KPIs.
  • Question any feature about to be built if it doesn't carry a clear purpose.

Unfortunately the most important feedback loop in software development is not purely technical, thus developers should have a more active role trying to make sure the loop is always closed.

I'd be interest to hear what other people are trying on that front.

Coaching at codebar.io

Last night I had a chance to coach at a Codebar event for the first time. They offer free basic programming and web development workshops to people who are underrepresented in the tech industry.

For me it was a great reminder of how difficult it is to learn how to code. Even though my student was following a "getting started" tutorial, the amount and complexity of new concepts involved was overwhelming at times. Luckily there were always small rewards (for both of us, mind) when each new concept "clicked" after getting a particular set of commands working.

For example:

basket = {"apple" => 3, "banana" => 1}
cities = ["London", "New York"]

puts basket["apple"]
puts cities[0]

cities.each { |city| puts city }

Looking at this simple piece of code, there are many syntax concepts we just take for granted:

  • The [ and ] can be used to define an Array and to access elements in both Arrays and Hashes.
  • Sometimes { and } are used to define Hashes. Other times those are actually blocks.
  • The |city| is a parameter to that block, but for methods we define parameters using ( and ).

Even the words I've just used to describe them (access, element, define, parameter, block etc) are a whole new vocabulary to people new to programming. The tutorials do their best to introduce them gently, but there's no escape that programming at the beginning will be challenging.

I can't remember the last time I had a chance to see someone take their first steps in programming but being there to make their path easier was definitely worth it.

Also, thanks to Samir for mentioning it and despo for organising it. I hope to be able to do it again soon.

3 key skills for software design

On his talk at the Java Zone 2014, Kent Beck listed what he considers to be key skills in software design. According to him, they don't come naturally to programmers, but can be learned. Those skills are:

Tolerance for ambiguity

Developers must be able to accept that their design will never be completely clean. There'll always be forces pulling it in different directions and assuming there's only one "correct" solution will most likely just lead to frustration.

Ability to wait

Ambiguity can only be tolerated when developers are able to postpone a design decision to the last responsible moment. Many will be tempted to jump to a particular design change too soon. Or worse, ignore relevant design aspects completely.

Treating design as a social process

That's what Kent considers to be the biggest cosmical joke on programmers. Effective design requires an understanding of the human side of software. The skills and dynamics of the team will inevitably affect how software is made. And grasping what the next person is going to think when they see the solution requires as much social as technical skills.

Personally, I'm not sure if the first two can exist separately. The last I consider to be the most important, and definitely the hardest.

Back from the ashes

It's been over two and a half years I don't post anything here. In the meantime I've used Github Pages and Jekyll mainly as a backup of my old posts.

Today I've found that I was running a version so old of Jekyll that it was breaking this site almost completely, so I've decided to upgrade it and apply the Lanyon theme so it looks at least half decent.

All the old posts are still available in the Archive page.

If you saw this post in your RSS reader:

  1. Thank you for staying around (or for just not cleaning up your feed that often)
  2. Let me know if anything breaks!