This is my favourite way of introducing functional decomposition, at best when already familiar with TDD. And it works as an eye-opener because TDD, when applied ’blindly’ on this problem, can easily lead to convoluted solutions in spite of any attempts to refactor.
So, we’re to generate something like this:
A
B B
C C
D D
C C
B B
A
For contrast, start off in the ’typical’ approach of TDD – start with A, then B, then C, … -, have a good struggle, then stop at half-time, assess the outcome and restart. This time begin by precisely describing the problem statement in words, as if explaining over the phone. What we expect, is…
- a sequence of letters from ’A’ up to a given letter
- arranged diagonally in a square, padded with blanks
- mirrored to the left, not repeating the first column
- mirrored down, not repeating the bottom row
Now we can take on the slices one by one, beginning with the sequence of letters. Being in Ruby, we’re going to simply extend the String class.
class String
def sequence_letters
'A'.upto(self).reduce(&:+)
end
end
Short and sweet! Then we move on to the second slice, noting that the input here can be any string.
class String
def spread_diagonal
chars.each_with_index.map { |c, i| (' ' * (length - 1)).insert(i, c) }.join("\n")
end
end
Short and sweet! Then we move on to the third slice, noting that this too works on any string.
class String
def mirror_left
each_line.map { |l| l.chomp }.map { |l| l.reverse + l[1..-1] }.join("\n")
end
end
Short and sweet! Then we move on to the final slice, noting that this too works on any string.
class String
def mirror_down
self + "\n" + each_line.map { |l| l.chomp }.to_a.reverse[1..-1].join("\n")
end
end
Short and sweet! And now all that’s left is composing these, leaning back and marvelling at the clarity.
class String
def diamond
self.letter_sequence.spread_diagonal.mirror_left.mirror_down
end
end
Now, there are many ways to solve the challenge, but this solution is incredibly powerful due to…
- lack of explicit flow control and branching (no if, for, while, …)
- usage of standard library functions and value expressions only
- no shared state, a purely functional data transformation pipeline
- functions with permissive preconditions and strict postconditions
- encapsulated building blocks, reusable in orthogonal composition
And this is a great time to reopen the question you’ve surely discussed before, how much thinking is one supposed to have in TDD. This exercise is a clear demonstration that functional decomposition may be hard to attain on the mechanical, heads-down, YAGNI path, and that breaking down the problem in a divide-and-conquer approach can lead to a smooth flow of well-compartmentalised deliverables, each with clear responsibilities and interfaces, resulting in a "good design".
PS: being a fan of Elixir’s pipe operator, |>, the perfect tool for any data transformation pipeliner, I couldn’t help but had to make this look better still, so after patching String with…
def >>(method)
self.send(method)
end
I could write…
def diamond
self >> :letter_sequence >> :spread_diagonal >> :mirror_left >> :mirror_down
end
Code is art!