A false start
Warning: The first part of this is a false start. But we often make false starts: they inform our work. Without the false start, I might not have had the insight that comes in the second phase. Or, some would argue, if I hadn’t started coding so soon, I might have had it sooner. We all get to decide what to do. The trick is to learn to listen to the code and to the brain and do the best thing we see. Go here to skip the false start – but I recommend you at least scan it. All the good mistakes are in there.
So there I was, trying to take a well-deserved nap and a picture came into my head. It was fuzzy, but I thought about it, and I’m sure it’s brilliant. It was something like this:
I think of the orange as a big sea of “NO”, the white lines just outlining a rectangle, and the black lines as meaning “YES”. Then imagine a huge matrix of rows, first all A’s, then B’s, then C’s, right on down to Z’s, then Y’s, X’s, right back to A. Then use the YES/NO matrix to stamp into the alphabet matrix. You’d get something like this:
| A | | B B | | C C | |D D| | C C | | B B | | A |
Except really huge, all the way from A to Z and back. For any desired solution to the Diamond Problem, you’d just slice this matrix and reflect it. Or maybe you’d only build one quarter of it and reflect twice. I don’t know, it’s just an idea.
I imagine a sort of array-oriented or block-oriented solution. But I really don’t know. Just have to try it. Here goes:
def test_stamp expected = [ '---A---', '--B-B--', '-C---C-', 'D-----D', '-C---C-', '--B-B--', '---A---'].join('') stamper = DiamondStamper.new(4) assert_equal(expected, stamper.stamp) end
That’s right: I’m just creating a big test. I’m going to see if I can build the program “by intention”. If it doesn’t seem to be going well, or as sub-problems arise, I’ll write more tests. If all else fails, I’ll give up and face the moral dilemma of whether to publish this.
Note that expected is just one big long string, not an array. I built it from an array so I could see that it was correct but my mental model at this point is that I’m just going to have a big two-dimensional space.
I’ve sketched the DiamondStamper this way:
class DiamondStamper def initialize(n) end def stamp paint = create_paint() stencil = create_stencil() stencil(paint, stencil) end def create_paint() "big string of ABCs" end def create_stencil() "big matrix or string of booleans" end def stencil(paint, stencil) # "and" paint and stencil to create the answer "not working yet" end end
This is perhaps a big bite, but I’m not really doing small-scale TDD here. I have a “big idea”. Perhaps I should be clever enough to break it down and drive it out but I feel the need to work top down for now, so the bites are bigger, at least at first.
Now let’s look at what the methods are supposed to do.
create_paint builds a long string of AAA BBB CCC etc. I can test that:
def test_paint expected = 'AAAAAAABBBBBBBCCCCCCCDDDDDDDCCCCCCCBBBBBBBAAAAAAA' stamper = DiamondStamper.new(4) paint = stamper.create_paint() assert_equal(expected, paint) end
This fails as expected:
2) Failure: test_stamp(TestDiamond2) [/Users/ron/Programming/diamond2/diamondtest2.rb:26]: <"---A-----B-B---C---C-D-----D-C---C---B-B-----A---"> expected but was <"not working yet">.
Now “all we need to do” is create that string. Can you feel the weight of the future bearing down on you? After we do this, we’ll have to create the stencil, in some form that can be “anded” with this structure to create the output. Both those are larger than this problem, and I can feel them looming.
This is the standard problem when we take a big bite top-down approach. It may go smoothly but for a while there’s a lot of weight waiting to be lifted. I’ll press on. The paint should be a simple nested loop. I hope. Here’s a first cut at the first part:
def create_paint() result = '' (0...@size).each do |row| (0...2*@size-1).each do |col| result << @alphabet[row] end end result end
That fails as I had intended:
1) Failure: test_paint(TestDiamond2) [/Users/ron/Programming/diamond2/diamondtest2.rb:13]: <"AAAAAAABBBBBBBCCCCCCCDDDDDDDCCCCCCCBBBBBBBAAAAAAA"> expected but was <"AAAAAAABBBBBBBCCCCCCCDDDDDDD">.
It was “easy” to generate the top side. Now the question is whether to build the bottom side with a loop or what. And of course we could code the double loop in other more clever ways but that’s for when we refactor. I could do the slice/reverse trick here to get the bottom part.
But you know what? There’s not much learning in that. I’m going to fudge the test to accept this much and move on to the YES/NO and the “and” operation, where the trouble lies.
def test_paint expected = 'AAAAAAABBBBBBBCCCCCCCDDDDDDDCCCCCCCBBBBBBBAAAAAAA' expected = 'AAAAAAABBBBBBBCCCCCCCDDDDDDD' # fudge or design change. wait and see. stamper = DiamondStamper.new(4) paint = stamper.create_paint() assert_equal(expected, paint) end
This feels very loosey-goosey to me. Slap-dash. Half-assed. Pick your own term, but this feels like I’m building a big pile of trouble. But we’re here to learn, so the big fool presses on. Here’s a test for the creation of the stencil:
def test_stencil expected = 'NNNYNNNNNYNYNNNYNNNYNYNNNNNY' stamper = DiamondStamper.new(4) stencil = stamper.create_stencil() assert_equal(expected, stencil) end
Very similar pattern. Fails of course, no surprise there. But it gets me thinking. Suppose we had an algorithm to produce this stencil. Basically it would be stuffing an N or Y into the string based on, well, based on the algorithm. There’s a boolean decision going on in there. Given that we have the boolean, then we know whether we want a letter. In the create_paint, we have a way of picking the letter we need. So these two things might be merged.
Now this is interesting. My initial idea was just this big stamp slamming down and cutting out letters in a kind of array operation. In J, one might just do that. (No doubt some of my J-using followers are running to their machines already. If they do, I’ll link to their articles here.)
What you’d do in J, I think, is think of a formula that decides whether to pick a letter or not, wrap it into a matrix, and fetch the letters. So the question is, what’s that formula?
Let’s look at our answer again:
0123456 0 ---A--- 1 --B-B-- 2 -C---C- 3 D-----D 4 -C---C- 5 --B-B-- 6 ---A--- 0123456
I’ve added in some row and column numbers, thinking that numbers are important here.
Oh! (Or, perhaps I mean “Oh, hell, this is all wrong”). Consider this graph:
| E | D | C | B |A +----------
If we draw it that way, it’s obvious: The letters show up when x == y. And the letter that shows up is the y-th letter from the alphabet. This is either irritating, because I’ve wasted literally minutes, or wonderful because it’s a clean answer. So what we’re going to do is tear out the objects and start over.
Here’s the new start:
require 'test/unit' class TestDiamond2 < Test::Unit::TestCase def test_hookup assert_equal(3, 2+1) end def test_quadrant expected = [ 'A---', '-B--', '--C-', '---D'].join('') quad = DiamondQuadrant.new(4) assert_equal(expected, quad.quadrant) end end class DiamondQuadrant def initialize(n) @n = n end def quadrant "hello" end end
Test fails, of course, because “hello” just won’t cut it. Time to code:
class DiamondQuadrant def initialize(n) @n = n - 1 @alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' end def letter(x,y) (x==y)?@alpha[y]:'-' end def quadrant result = '' 0.upto(@n) do | y | 0.upto(@n) do | x | result << letter(x,y) end end result end end
Test passes! We’re two reflections away from the solution. Maybe a little more fiddling if we want an array of strings or interpolated newlines.
A little explanation might be in order here …
I almost “just typed this in”, but not quite. I was so much on a roll, though, that I didn’t pull out to explain what I was doing. Let me reconstruct what happened.
I really did just type in the nested loop in quadrant. That’s because the array model told me we were just going to create an array and nested loop is how you do that. Inside the loop, I “remembered” that I was appending to the string, so I wrote the
result << letter(x,y). That’s definitely programming by intention. I intended to append the next letter. I knew the letter depended on x and y, because I wanted dash if x != y and a letter otherwise. So then I wrote the letter function, this way:
def letter(x,y) (x==y)?alpha(y):'-' end
I had a function
alpha in there to begin with, but of course it was a straight fetch from the alphabet string. So I moved creation of the string to initialize and plugged that into the ternary operator:
def letter(x,y) (x==y)?@alpha[y]:'-' end
Why the ternary operator? Because I’m old and because it pisses Chet off when I use it. That’s reason enough. I find it readable and compact. Would you prefer it spaced out like this?
def letter(x,y) (x==y) ? @alpha[y] : '-' end
Or, your team’s coding standard might not allow the ternary. My team prefers it.
Anyway, there we are.
I could go on, of course, and do the reflections to get the Diamond. But the main discoveries have already been, um, discovered. What were they?
The Vision In A Dream of the stencil stamper was apparently a dead end. But I had thought part-way through several ways of doing it before I started, and it seemed very appealing. When I started coding, the complexity and weight of it began to bother me early. Finally, I took a break, and as soon as I did, I realized “I can’t go forward with this: it’s not going to be good”.
So I switched direction.
If anything, this experience may argue for less thinking up front, or, more accurately, for testing flashy ideas early. And that’s really what happened, I got up from my nap, started typing, got in trouble, switched direction, recognized that one symmetry of the picture is ideal because of the
x == y aspect, then went ahead and did it.
Would this version be “better” than the other version, if finished? It just needs a couple of reflections and such. I’m going to apply the “Ignorance and Apathy” rule here:
I don’t know, and I don’t care.
We’re here to think about thinking, how much and when. First time through, very little up front thinking, lots of nice refactoring, smooth development from beginning to end. Even if it did take 6000 words to write it up. This one, more abstract thinking, lovely visionary design, big bite to implement, and I collapsed under the weight of my idea.
You’re much stronger than I am. You might have pushed it through. Would you be glad that you had? Would you have a truly better result?
We can never really know. Each time we try this is conditioned by all our prior experience, especially with this problem.
For me, I’ll continue to push to code as early as possible. I like to know what the code thinks about my ideas.