Skip to content

Instantly share code, notes, and snippets.

@Vaguery
Created December 7, 2014 20:10
Show Gist options
  • Select an option

  • Save Vaguery/9d339a4c8c33d4ce63f0 to your computer and use it in GitHub Desktop.

Select an option

Save Vaguery/9d339a4c8c33d4ce63f0 to your computer and use it in GitHub Desktop.
date 2014-12-07

{::options coderay_line_numbers="nil" /}

Making a habit of the "diamond kata"

I noticed the other day that there's been some interest in my social network (specifically in the agile coach clump) about a certain exercise that folks are calling the "diamond kata". I thought I might give it a go as well.

"Noticed" is noteworthy, since what really happened is that in the process of working with Ron on his new website, I saw one of his narratives on the kata pop up, and it caught my attention. Not because I'd been missing the fun of doing code katas; rather, when I glanced at the problem itself, and then glanced at Ron's and peeked at George Dinwiddie's and then Alistair Cockburn's discussions, I was all like, "Hey, this is obviously a Lindenmayer system, not some kind of array thingie."

That response, in turn, is noteworthy, because Ron and I---who have been pairing recently---have mentioned to one another how weird a lot of the discords are when we pair. "Why would anybody do it that way?" kind of discords, nothing bad or argumentative, but just not simultaneously seeing the same "simple" path from the perceived problem at hand to the solution that emerges from the coding. And then that clicked and became noteworthy because my peek at the Cockburn Variation made me realize there's something I've never heard the agile community speak on: habit.

So the other day I mentioned how startled I was to realize agile values and practices don't even seem to mention habit, while to me it's a core aspect of cognition and organizational behavior. And we found more or less that to him, "habit" is considered the "mindless" part of programming and software development which is the very thing much of agile methodologies are intended to disrupt and transform into mindfulness, whereas to me "habit" is a core notion of Dewey's Pragmatism, and a wonderful conceptual tool for undermining some of the bad folk psychology and institutional crap that arises from the conservative cult of "personal responsibility".

Dan Little has put together a great summary of more or less the same things I understand about "habit". And the fact that this summary was the first to pop out of my Googling is noteworthy, to me, because I just met Dan the first time a few weeks back, even though we're local to one another. And I met him at a workshop hosted by Scott Page, an old friend of mine whose work on diversity is probably going to be salient as well.

Having tried to develop a habit of cutting short these little chains when they drop down into pure personal experience, I shall summarize: diamond kata, I dunno what it should be exactly but when I look at it it sounds different to me than it does for other folks I know, discussion about mindfulness and habit and test-driven design and thinking before writing code, diversity of backgrounds and mental toolkits not mentioned explicitly in agile methodologies.

So there you go.

A word on TDD and these little narratives

Some readers (folks not in the agile parts of my social network, for example) may not be 100% clear on what a "code kata" is, and may be a bit fuzzy on what "TDD" actually means.

A code kata is a little trivial-sounding exercise, often about the size and difficulty of a first-semester programming class's homework problem, or maybe a mathematical recreation from a Martin Gardner column or something. They're often done in public and by groups of software developers in a coaching or educational situation---not to practice "coding" but rather to practice mindfully of one or more techniques of problem-solving.

In this case, Alistair's initial note seems to have pointed us all at "traditional" test-driven design vs "thinking and designing a bit beforehand" (note: I haven't read his piece in detail yet, as of this writing). Now in my experience a lot of folks who haven't been immersed in a TDD approach seem to think it means something like "try real hard to write unit tests as you go", or more rarely "never write code until you have a unit test for the thing you're writing". But TDD as such implies a much more rigorous stance: one in which the core work cycle of "red-green-refactor" should in a real way drive the design of the software you produce. "Red" meaning "write a failing check", "green" meaning "add only enough code to make that (and all) checks pass", and "refactor" meaning "refactor, dammit".

Done with even moderate rigor, "strict" TDD can feel awful to Very Smart Programmers who already think they have a good idea of where they're going. Not least because of that restriction "only enough code"; to trot out a trivial example I've actually seen: You know this new function is supposed to return the square root of its argument. You add a failing check that says when the argument is "16" the response should be "4". But then it hurts when you make the single failing unit test pass by writing the method so that it always returns 4. Why would you do something that stupid? Because the next failing check, say for an argument of 25, will make you revise the method so that it's more fully correct.

Now that's a cartoon in a way, but the point is to focus one's attention on making small enough changes to the codebase, each heavily supported by frequent checks, so that refactoring or restructuring it can be easier for your "future you" or any other colleague. It's painful because it emphasizes how infrequently we have a warrant to make even trivial assertions about what something we've envisioned and just made "does". It's painful because it calls into question our habitual tendency to say, "Clearly, this is an X!" and then blithely build an X that doesn't do quite what we expected in context.

Note: I may be wrong about some or all of this. Because the other thing I haven't mentioned is these little narratives I write. They're accounts of the work being done as I go, and while I sometimes check for spelling and suchlike, they're rarely edited. I'm literally adding to this account while I write and execute code. They are, as Brian Marick has done a good job summing up, a manglish way of talking about a manglish way of working. And as for the Mangle of Practice, we will leave that for another day.

So...

Clearly, this is a parametric 0L system!

So he "diamond kata" comes into the mix from Seb Rose, and is summed up pretty simply as:

When I say A, you (who are the function) say

A

and when I say B, you say

-A-
B-B
-A-

and when I say C, you say

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

"And so on". Which I take to mean that if I say T then your result would be a big-ass square with a load of hyphens (nominally spaces, but hyphens I can see) in it, and something like 39 (?) rows and columns. I don't really know what's supposed to happen if I say 7 or HI THERE, but apparently that's not important right now.

Now when I glance at this, it's a thing about strings. That is, the inputs look like strings, and the outputs look a lot like strings. Interestingly (and one of the noteworthy points that made me yammer on so much already), other folks seem to see arrays. Ain't that weird?

But no, I see quite clearly the output for B as -A-\nB-B\n-A-. And I'm also reminded immediately of 30 years of playing with Lindenmayer systems as a theoretical biologist, because the sequence of outputs stacks up interestingly and looks to me like one of the many string rewriting diagrams I made for academic posters and thesis work and crap like that. This progression:

                (A)
          (-A-)(B-B)(-A-)
(--A--)(-B-B-)(C---C)(-B-B-)(--A--)
...

looks a bit like

            (seed)
      (leaf)(stem)(leaf)
(leaf)(leaf)(stem)(leaf)(leaf)
...

with some other stuff happening inside those "cells" to make them grow and get pushed out from the center towards the periphery... just like in the canonical Anabaena simulation Lindenmayer himself wrote.

This sounds like me thinking a lot, but really it's me trying to be clear as I explain what I'm seeing quickly---almost immediately---upon hearing the problem statement. Because I don't have quite the same background everybody else does.

Day One: Yak

I feel I should immediately write a test. I set out to do so... and spend an hour or two getting Sublime Text and the Ruby Test package working more or less the way I remember they used to, but which are now broken because I recently updated my laptop to Yosemite, but then RVM needs updating and some other work, oh and I don't actually remember the right keystroke to run tests inside Sublime (Cmd-T), and I eventually discover there's a secret setting (mentioned right on the front github project page, but secret from me) in the Ruby Tests package that I need to toggle ("check_for_rvm": true) so that it actually works with RVM, and then my spec files need to be in a folder called spec and... yak shaving.

Day Two: Red

Where was I? Right, clearly this is an L system.

There's actually a lot going on in these three discrete steps of string-rewriting I've sketched above. There's the way the central "stem" pushes out the "leaves"; there's the way all the cells "expand" within themselves; there's also something about the stem incrementing its end letters from A to B to C and so on.

I wonder if I can get the first line to work. I start by writing a failing expectation (I'm using rspec, so the small checks I'll write are "expectations") that invokes the code I "wish I had" (at the moment):

describe "first row" do
  it "should return 'A' for input 'A'" do
    expect(diamond('A').lines[0]).to == 'A'
  end
end

In other words, there is some method diamond that produces a thing. It fails not because the outcome isn't what I "really want", but because there ain't no such method yet.

Failures:

  1) first row should return 'A' for input 'A'
     Failure/Error: expect(diamond('A').lines[0]).to  == ''
     NoMethodError:
       undefined method `diamond' for #<RSpec::ExampleGroups::FirstRow:0x007fe5591a3d58>
     # ./spec/diamond_spec.rb:6:in `block (2 levels) in <top (required)>'

So I add

def diamond
end

...

ArgumentError:
  wrong number of arguments (1 for 0)

Oh right...

def diamond(char)
end

...

NoMethodError:
       undefined method `lines' for nil:NilClass

...

def diamond(char)
  return char
end

...

ArgumentError:
  The expect syntax does not support operator matchers, so you must pass a matcher to `#to`.

Dagnabbit. There's this newfangled rspec expect syntax I'm not used to yet...

describe "first row" do
  it "should return 'A' for input 'A'" do
    expect(diamond('A').lines[0]).to be == 'A'
  end
end

Which passes.

In other words, the diamond(char) method returns the argument.

And so on?

As an aside, this sure feels like writing it down isn't the best medium. Along the way I've also created a little Sublime snippet that helps me type fenced code blocks easier for the kramdown renderer I prefer.

Seems "obvious" that the next step should be a longer line.

  it "should return '-A- for input 'B'" do
    expect(diamond('B').lines[0]).to be == '-A-'
  end

...

Failures:

  1) first row should return '-A- for input 'B'
     Failure/Error: expect(diamond('B').lines[0]).to be == '-A-'
       expected: == "-A-"
            got:    "B"

Not surprising. But... an interesting question arises. Is the input B to be interpreted as a "counter"---which is to say as a "fake 2"---or as the character proper?

I could punt here, and write a case statement that returns the expected line for each letter in turn. But I don't see a way out of that mess with the information on hand already. No---it just don't feel right.

I mark that second expectation "pending" for a minute.

  it "should return '-A- for input 'B'" do
    pending 
    expect(diamond('B').lines[0]).to be == '-A-'
  end

Growing out

Like I said, there are a bunch of things happening all at once in each "tick" of the clock here: there are more lines each step, they all get longer, and (as I see it) the middle one has a new character in it that marches up in lexicographic order... and (I realize here) which exactly matches the argument.

I'm already envisioning the returned result as the terminal stage of an iterated rewriting system. In other words, the way I see it, each call launches a new unfolding of the "seed" A to some later stage in development of "stem" and "leaves". The argument B or T or whatever is in some sense "for" all three features (columns, rows, characters), but in some other way it's closest to the surface in the middle line.

describe "middle row" do
  it "should contain the argument character" do
    expect(diamond('B').lines[1]).to include 'B'
  end
end

...

Failures:

  1) middle row should contain the argument character
     Failure/Error: expect(diamond('B').lines[1]).to include 'B'
       expected nil to include "B", but it does not respond to `include?`
     # ./spec/diamond_spec.rb:17:in `block (2 levels) in <top (required)>'

Ah, right. There is no second line of the result. If I'm going to pursue this route, I need the result to be longer (at least).

describe "number of rows" do
  it "should have 3 rows for argument 'B'" do
    expect(diamond('B').lines.count).to eq(3)
  end
end

...

  2) number of rows should have 3 rows for argument 'B'
     Failure/Error: expect(diamond('B').lines.count).to eq(3)
       
       expected: 3
            got: 1
       
       (compared using ==)
     # ./spec/diamond_spec.rb:23:in `block (2 levels) in <top (required)>'

Well, I don't see any real reason not to grow the thing right now. How about if I just add new lines to the returned string, incrementing the letter, until it contains the argument character?

def diamond(char)
  result = ''
  stem_letter = 'A'
  until result.include?(char)
    result += "#{stem_letter}\n"
    stem_letter = stem_letter.succ
  end
  return result
end
  2) number of rows should have 3 rows for argument 'B'
     Failure/Error: expect(diamond('B').lines.count).to eq(3)
       
       expected: 3
            got: 2
       
       (compared using ==)
     # ./spec/diamond_spec.rb:23:in `block (2 levels) in <top (required)>'

Right, right. I forgot the two-directional expansion of the string. Let me fix that, and then deal with refactoring this awful little thing afterwards.

def diamond(char)
  result = 'A'
  stem_letter = 'A'
  until result.include?(char)
    result.gsub(stem_letter,"#{stem_letter}\n#{stem_letter.succ}\n#{stem_letter}")
    stem_letter = stem_letter.succ
  end
  return result
end

And boom there's a damned brain-fail in there that eventually makes me force-quite my text editor. Turns out, when you use Strin#gsub, you want to record the result somewhere, otherwise it gets thrown away and you have an infinite loop.

First, it's interesting to note that this isn't even a testable bug, since it crashes the testing system itself. Second, it's funny because I'm implementing a recursive system with a termination condition and I fucked up the recursion part instead of the termination condition part.

Fixing that:

def diamond(char)
  result = 'A'
  stem_letter = 'A'
  until result.include?(char)
    result.gsub!(stem_letter,"#{stem_letter}\n#{stem_letter.succ}\n#{stem_letter}")
    stem_letter = stem_letter.succ
  end
  return result
end

and all of a sudden all the (non-pending) expectations pass.

But it's hideous. Before this first pomodoro is done (and even before I "un-pending" that last expectation), I should refactor it.

At this point, I don't see a lot to do except for making the code a bit cleaner (and shorter), and using more appropriate variable names.

def diamond(termination_signal)
  stem = 'A'
  whole_result = 'A'
  until whole_result.include?(termination_signal)
    next_stem = stem.succ
    whole_result.gsub!(stem,"#{stem}\n#{next_stem}\n#{stem}")
    stem = next_stem
  end
  return whole_result
end

Something tells me the change from stem_letter to stem is a hint of where I'll end up, but we'll see. Almost certainly there is a class or two in the making, there. And that line next_stem = stem.succ feels like it will very soon end up being important....

The nice thing about having even these few tests is that I know (and knew at every little step along the way) when I made a stupid mistake.

Un-Pending

I take a good long break after that first half-hour. It's a weekend, after all. When I come back, at least I see the pending sitting there.

Actually, I see a lot more:

describe "first row" do
  it "should return 'A' for input 'A'" do
    expect(diamond('A').lines[0]).to be == 'A'
  end

  it "should return '-A- for input 'B'" do
    pending 
    expect(diamond('B').lines[0]).to be == '-A-'
  end
end

describe "middle row" do
  it "should contain the argument character" do
    expect(diamond('B').lines[1]).to include 'B'
  end
end

describe "number of rows" do
  it "should have 3 rows for argument 'B'" do
    expect(diamond('B').lines.count).to eq(3)
  end
end

Yuck. Bunch of misstatements in there. I give it a quick polish until it becomes more like this. I also notice I need to practice my rspec matcher idioms, so I replace to be == 'A' with to eq('A')

describe "simplest input 'A'" do
  it "should return 'A' for input 'A'" do
    expect(diamond('A')).to eq('A')
  end
end

describe "subsequent generations" do
  describe "top row" do
    it "should be '-A- for input 'B'" do
      expect(diamond('B').lines[0]).to eq('-A-')
    end
  end

  describe "middle row" do
    it "should contain the argument character" do
      expect(diamond('B').lines[1]).to include 'B'
    end
  end

  describe "number of rows" do
    it "should have 3 rows for argument 'B'" do
      expect(diamond('B').lines.count).to eq(3)
    end
  end
end

Maybe just a matter of taste, but that's a lot clearer to me.

The failure message is now

  1) subsequent generations top row should be '-A- for input 'B'
     Failure/Error: expect(diamond('B').lines[0]).to eq('-A-')
       
       expected: "-A-"
            got: "A\n"

Right. The row gets longer.

The first transition here was A -> -A-. What if when I rewrite the stem, I also insert those hyphens?

# inside diamond()
    whole_result.gsub!(stem,"-#{stem}-\n#{next_stem}\n-#{stem}-")

...

  1) subsequent generations top row should be '-A- for input 'B'
     Failure/Error: expect(diamond('B').lines[0]).to eq('-A-')
       
       expected: "-A-"
            got: "-A-\n"

Well, OK. I guess.

  describe "top row" do
    it "should be '-A- for input 'B'" do
      expect(diamond('B').lines[0].strip).to eq('-A-')
    end
  end

So that works. Does it scale up?

    it "should be '--A--' for input 'C'" do
      expect(diamond('C').lines[0].strip).to eq('--A--')
    end

Nope! Fails because, as the method's written right now, the hyphens are just added to the immediate neighbors of the "stem", not every "leaf".

So yeah, there's persistent state information in every "leaf" (and the "stem", while we're at it). I actually need to update every line, and that's not unexpected but there's no automated expectation that captures it yet.

I mark that failing expectation pending and add this one

  describe "row lengths" do
    it "should be the same" do
      big_G = diamond('G')
      big_G.lines {|line| expect(line.strip.length).to eq(7)}
    end
  end

I'm feeling the confidence to jump way out here, and try a G input. What happens:

  1) rewriting: row lengths should be the same
     Failure/Error: big_G.lines {|line| expect(line.strip.length).to eq(7)}
       
       expected: 7
            got: 3

Not a lot different in effect from the failing diamond(C).lines[0] -> '--A--', but a bit more comprehensive.

I page back through these notes1 and fine my sketch that looks like this:

            (seed)
      (leaf)(stem)(leaf)
(leaf)(leaf)(stem)(leaf)(leaf)
...

Makes me think, that does. Because when I say I want "every line" to be updated at every step of the developmental clock, what I guess I mean is that the "diamond" is a list of those "cells" in a more proper way than just an informal hand-wave from "line of string" to "cell".

In other words: I feel a class coming on. But let me get all these expectations passing, before I do this weird "Extract Subtring to Class" thing I'm envisioning. I need to make a painful change, to get that to happen; maybe the extra pain is really why I'm adding a class structure here.

Right now I'm doing (at least) two featury things in one line of code:

    whole_result.gsub!(stem,"-#{stem}-\n#{next_stem}\n-#{stem}-")

This one line both expands the stem and extends the lines adjacent to the stem. Let me see if I can extract those to two different methods. If I start with adding hyphens to the ends of each leaf line, I'll build some kind of method that looks a bit like this

def expand_leaves(diamond)
  # do stuff
  return wider_diamond
end

It's starting to get painful thinking of these as free-standing methods in the Ruby interpreter's root namespace. But I'm pulling things out before building those very classes, so maybe I ought to bear down and see what pops out.

Even if I'm going to refactor it away, let me write an expectation here

describe "expand_leaves" do
  it "should return a string with extra hyphens added to each line" do
    narrow = "a\n-b-\nc--c\nddd"
    wider = "-a-\n--b--\n-c--c-\n-ddd-"
    expect(expand_leaves(narrow)).to eq(wider)
  end
end

And I get pretty close!

def expand_leaves(diamond)
  wider_lines = diamond.lines.collect do |line|
    "-#{line.strip}-"
  end
  return wider_lines.inject("") {|diamond,line| "#{diamond}\n#{line}"}
end

fails with the encouraging error

  2) expand_leaves should return a string with extra hyphens added to each line
     Failure/Error: expect(expand_leaves(narrow)).to eq(wider)
       
       expected: "-a-\n--b--\n-c--c-\n-ddd-"
            got: "\n-a-\n--b--\n-c--c-\n-ddd-"

I add the perennial String#strip, and it passes. Now maybe I need to call that somehow in diamond()?

def diamond(termination_signal)
  stem = 'A'
  whole_result = 'A'
  until whole_result.include?(termination_signal)
    next_stem = stem.succ
    whole_result.gsub!(stem,"#{stem}\n#{next_stem}\n#{stem}")
    whole_result = expand_leaves(whole_result)
    stem = next_stem
  end
  return whole_result
end

def expand_leaves(diamond)
  wider_lines = diamond.lines.collect do |line|
    "-#{line.strip}-"
  end
  return wider_lines.inject("") {|diamond,line| "#{diamond}\n#{line}"}.strip
end

Which fails with

  2) rewriting: row lengths should be the same
     Failure/Error: big_G.lines {|line| expect(line.strip.length).to eq(7)}
       
       expected: 7
            got: 13

And dagnabbit. Of course the lines in diamond('G') should actually be 13 characters long! There's a character added on both ends of the first one, six times.

But now the next line of the diamond appears to be 12 characters long. Which is odd---well, odd because it's not right, but also because it's not odd. Also hang on another minute here. I suddenly see I've been setting the "stem" line to a single character this whole time.

I'm right now tempted to rip out the whole method and start over. But I sense what's really happening is that I'm juggling a half-made class of "cells" with low-level string manipulations, and responsibility is spread out all over the whole mess. Messily.

A leaf is a leaf is a leaf

Fukkit, I'm making a Leaf class.

I tear out the expand_leaves method entirely, and also its expectation, and add this one instead:

describe "Leaf" do
  it "should expand by adding hyphens both ends" do
    expect(Leaf.new('abc').expand).to eq('-abc-')
  end
end

A few things happening here all at once: I'm creating a Leaf class, and it takes an initialization argument, and it has an #expand method, and it looks an awful lot like a line of my current string-based diamond. I sense I'm moving towards a more behavior-driven style here; what I have in mind is to treat this as "the code I wish I had", and to incrementally fill it in using unit test expectations until it (and everything else) passes.

Doing that very thing, we need a Leaf class.

  2) Leaf should expand by adding hyphens both ends
     Failure/Error: expect(Leaf.new('abc').expand.inspect).to eq('-abc-')
     ArgumentError:
       wrong number of arguments (1 for 0)

It needs an initializer

class Leaf
  def initialize(string)
  end
end

which fails

  2) Leaf should expand by adding hyphens both ends
     Failure/Error: expect(Leaf.new('abc').expand.inspect).to eq('-abc-')
     NoMethodError:
       undefined method `expand' for #<Leaf:0x007ff64111b4a0>
     # ./spec/diamond_spec.rb:45:in `block (2 levels) in <top (required)>'

so I make that

class Leaf
  def initialize(string)
  end

  def expand
  end
end

and the failure is something I can act on now...

  2) Leaf should expand by adding hyphens both ends
     Failure/Error: expect(Leaf.new('abc').expand.inspect).to eq('-abc-')
       
       expected: "-abc-"
            got: "nil"

What's a Leaf, after all, but a kind of fancy String with a few extra Lindenmayer System behaviors slapped onto it?

So here's what I end up with

class Leaf < String
  def expand
    "-#{self}-"
  end
end

exercised by this (passing) expectation

describe "Leaf" do
  it "should expand by adding hyphens both ends" do
    expect(Leaf.new('abc').expand).to eq('-abc-')
  end
end

Now let me use the damned thing.

Before, I was keeping the entire diamond in a single string. Now it starts to feel as though the "diamond" wants to be an Array of Leaf and Stem instances, with what may be a special #inspect method that prints out all the constituent cells as a single concatenated string.

A stem is not a leaf

I can't stand to push this into a high-energy transition state where there is a kind-of-string and a kind-of-Leaf all mixed up. So I want the Stem class as well.

Same behavior-driven approach there: I write a single expectation with a lot of implied code in it.

describe "Stem" do
  it "should expand by dividing into three cells" do
    expect(Stem.new('A').expand.length).to eq(3)
  end
end

By which I mean the result of Stem#expand should be an array of three items, two leaves and an "older" Stem in the middle.

class Stem < String
  def expand
    [Leaf.new(self),self,Leaf.new(self)]
  end
end

Let me check the Leaf instances first.

it "should produce expanded leaves" do
  expect(Stem.new('A').expand[0]).to eq('-A-')
  expect(Stem.new('A').expand[-1]).to eq('-A-')
end

To get those to pass, I make an obvious change, but also have to go back and fix a silent bug in the Leaf class, where I was returning a String instead of a Leaf instance. (Problem with subclassing core library items!)

class Leaf < String
  def expand
    Leaf.new("-#{self}-")
  end
end

class Stem < String
  def expand
    [Leaf.new(self).expand,self,Leaf.new(self).expand]
  end
end

Finally, I want the aging Stem to get wider as well as producing a new pair of Leaf instances.

  it "should age upon expanding" do
    expect(Stem.new('A').expand[1]).to eq('B-B')
    expect(Stem.new('C---C').expand[1]).to eq('D-----D')
  end

which leads to

class Stem < String
  def expand
    new_letter = self[0].succ
    new_string = new_letter + "-"*self.length + new_letter
    return [Leaf.new(self).expand,
      Stem.new(new_string),
      Leaf.new(self).expand]
  end
end

Growing

Anyway, I have classes in hand now, but still one failing expectation

  1) rewriting: row lengths should be the same
     Failure/Error: big_G.lines {|line| expect(line.strip.length).to eq(13)}
       
       expected: 13
            got: 3

I'm not actually using my new classes yet, so let me do so. It's actually not a very big jump; most of the typing is actually deletion, even without a cleanup:

def diamond(termination_signal)
  l_system = [Stem.new('A')]
  until l_system.detect {|cell| cell.include?(termination_signal)}
    next_step = l_system.collect {|c| c.expand }
    l_system = next_step.flatten
  end
  return l_system.inject("") {|str,c| "#{str}\n#{c}"}
end

In other words, "until some element of the array l_system contains the argument letter, just apply #expand to every cell and collect the results in a flat Array".

It doesn't work right away, but at least it doesn't spin out an infinite loop this time.

First failure is

  1) simplest input 'A' should return 'A' for input 'A'
     Failure/Error: expect(diamond('A')).to eq('A')
       
       expected: "A"
            got: "\nA"

Which is my old nemesis, String#strip again. I begin to suspect a conspiracy. I fix that by slightly modifying the returned value from diamond()... and all the expectations suddenly pass.

Now I really suspect a conspiracy.

I'd better look it over and make sure. Because that's actually kindof weird.

def diamond(termination_signal)
  l_system = [Stem.new('A')]
  until l_system.detect {|cell| cell.include?(termination_signal)}
    next_step = l_system.collect {|c| c.expand }
    l_system = next_step.flatten
  end
  return l_system.inject("") {|str,c| "#{str}\n#{c}"}.strip
end

class Leaf < String
  def expand
    Leaf.new("-#{self}-")
  end
end

class Stem < String
  def expand
    new_letter = self[0].succ
    new_string = new_letter + "-"*self.length + new_letter
    return [Leaf.new(self).expand,
      Stem.new(new_string),
      Leaf.new(self).expand]
  end
end

Gettin' classy all over the place

That's a spooky thing, but the code is still too dirty to get my head around entirely. Let me refactor and rename a bit.

Frankly, the biggest elephant in this (little teeny) room is the leftover weirdness of calling diamond() in the root namespace. I'd rather have a Diamond class... although I can see the call syntax being more like Diamond.from(char) rather than an explicit Diamond.new thing.

Why a Diamond class? Well, there's a step function sitting right there already, mixed in with the until loop. Let me try to untangle that.

def diamond(termination_signal)
  l_system = [Stem.new('A')]
  until l_system.detect {|cell| cell.include?(termination_signal)}
    l_system = next_step(l_system)
  end
  return pretty_l_system(l_system)
end

def next_step(l_system)
  new_cells = l_system.collect {|c| c.expand }
  return new_cells.flatten
end

def pretty_l_system(l_system)
  l_system.inject("") {|str,c| "#{str}\n#{c}"}.strip
end

Now, see, that's a couple-three methods and attributes right there. Gonna class that up right....

Later that same day...

And then I took a break for a couple of hours. When I sat down again, all I "really" did was replace the diamond() method with a Diamond.from() call, and give the Diamond class all the methods that I'd already extracted out of diamond() above.

Together with a few---no, really, just a half-dozen at most---little missteps involving simple typos that my expectations/checks caught for me, there was thing that kept pissing me off the whole time. Infinite loops.

See, an L-system is recursive, and acts a bit like what a computer scientist might imagine a grammar is like. But unlike the kind of grammars computer scientists tend to use, Lindenmayer systems rewrite every symbol on every step. That makes a few esoteric differences in the way the combinatorics work out in the end, and formal languages and blah de blah... but it makes a practical difference in how quickly and thoroughly an infinite loop explodes, combinatorially.

Turns out that if, for example, you make the mistake of writing the termination condition check in exactly the wrong direction, then you not only enter an infinite loop, but you fill your text editor's buffer so jam-packed full of hyphens and whatnot that even it refuses to respond. They you have to force-quit it, and maybe also kill -9 a runaway ruby process or two as well.

So apparently I can't even recursion. It's an interesting and not-at-all esoteric testing problem. Please make a note of it, and try not to use up all the characters in the multiverse if you forget to do it right.

But the code, and the expectations?

It looks like this:

class Diamond
  attr_accessor :cells

  def initialize
    @cells = [Stem.new('A')]
  end

  def step
    new_cells = @cells.inject([]) {|n,cell| n << cell.expand }
    @cells = new_cells.flatten
  end

  def reached?(termination_character)
    any_cell_has_one = @cells.detect {|cell| cell.include?(termination_character)}
    return !any_cell_has_one.nil?
  end

  def inspect
    (@cells.inject("") {|str,c| "#{str}\n#{c}"}).strip
  end

  def self.from(char)
    d = Diamond.new()
    d.step until d.reached?(char)
    return d.inspect
  end
end


class Leaf < String
  def expand
    Leaf.new("-#{self}-")
  end
end


class Stem < String
  def expand
    new_letter = self[0].succ
    next_string = new_letter + "-"*self.length + new_letter
    return [Leaf.new(self).expand,
      Stem.new(next_string),
      Leaf.new(self).expand]
  end
end

And here are the expectations I ended up with, using the new Diamond class.

describe "simplest input 'A'" do
  it "should return 'A' for input 'A'" do
    expect(Diamond.from('A')).to eq('A')
  end
end

describe "rewriting:" do
  describe "top row" do
    it "should be '-A- for input 'B'" do
      expect(Diamond.from('B').lines[0].strip).to eq('-A-')
    end

    it "should be '--A--' for input 'C'" do
      expect(Diamond.from('C').lines[0].strip).to eq('--A--')
    end
  end

  describe "middle row" do
    it "should contain the argument" do
      expect(Diamond.from('B').lines[1]).to include 'B'
    end
  end

  describe "number of rows" do
    it "should be 3 for argument 'B'" do
      expect(Diamond.from('B').lines.count).to eq(3)
    end
  end

  describe "row lengths" do
    it "should be the same" do
      big_G = Diamond.from('G')
      big_G.lines {|line| expect(line.strip.length).to eq(13)}
    end
  end
end


describe "Leaf" do
  it "should expand by adding hyphens both ends" do
    expect(Leaf.new('abc').expand).to eq('-abc-')
  end
end

describe "Stem" do
  it "should expand by dividing into three cells" do
    expect(Stem.new('A').expand.length).to eq(3)
  end

  it "should produce expanded leaves" do
    expect(Stem.new('A').expand[0]).to eq('-A-')
    expect(Stem.new('A').expand[-1]).to eq('-A-')
  end

  it "should age upon expanding" do
    expect(Stem.new('A').expand[1]).to eq('B-B')
    expect(Stem.new('C---C').expand[1]).to eq('D-----D')
  end
end

Also, while you're writing out your Do Not Do list, don't even start wondering in code what happens when you invoke this little wonder:

# do not run this code
describe "curiosity" do
  it "should do a thing but I don't know what" do
    expect(Diamond.from('ABC')).to eq('')
  end
end

Why should you not run that code? Can you figure out why? Might it have something to do with starting from a seed string A and using String#succ until you reach the string ABC? Ask your teacher if you're confused.

Welp.

I could refactor that last little thing, especially by renaming a bunch of things, and maybe being a bit more idiomatic by putting more function chaining into the block stuff. Whatever.

But you know what? I haven't actually checked whether the acceptance test passes. That was dumb, huh?

Stuff that's funny and weird:

  • The Diamond class is huge, compared with diamond(). I'm not sure how I feel about that, but I sense that might mean Diamond doesn't really need to be a class at all.

  • I found more ways to create infinite loops today than I have in the last five years.

  • Was this "TDD" in any strict sense? I am not a professional software developer, so I don't give a damn to be honest. It might have been flavored more behaviorally than some folks feel is warranted, or it might be undifferentiable. It wasn't so much rigorous as an authentic artisanal experience.

  • Words take lots of extra time. If I'd not been writing everything out longhand like a Very Boring Film Noir character, would it have taken me more than 20 minutes? As it worked out, it took about three hours total---not including the yak-shaving waste of a first day getting my computer set up the way I like it.

  • I should (and will now) read some of the other accounts. I'm curious about what in particular Alistair Cockburn means by "thinking" in his piece, and whether that's different from "bringing different experience to the table".

  • There's a line in the piece of Dewey, on habit, Dan Little quotes. I want to surface that in particular:

    Given a bad habit and the "will" or mental direction to get a good result, and the actual happening is a reverse or looking-glass manifestation of the usual fault---a compensatory twist in the opposite direction. Refusal to recognize this fact only leads to a separation of mind from body, and to supposing that mental or "psychical" mechanisms are different in kind from those of bodily operations and independent of them.

    I imagine folks who disavow "habit" are prone to lapse into unfruitful and potentially dangerous mind--body dichotomies. Bad business, those.

  • The thing I ended up building is basically a parametric 0L system after all. That is, the update rules for each "cell" are context-free (that's the "0L" part), but they maintain a persistent internal state (the "string", in my version). I can't really tell, looking back through a haze of several hours' thought and work, whether it might also have been possible to make a contextual L-system work: something on the level of characters rather than lines, where the neighboring characters (maybe including newlines) are sufficient to provide all the information needed.

    So for example it might have worked that '[null]A[null]' -> '-A-|B-B|-A-' and '|B-'' -> '|C--' and '|-B' -> '--C|' and so forth. Maybe. Doesn't feel like it would work at that scale, but it would be interesting if there were rules for refactoring both the code that runs L systems and the L systems themselves....

fin

Footnotes

  1. And along the way, I notice that it takes a huge amount of extra time for me to do the writing, compared with just the expectations and code. On the one hand, maybe I'm thinking more about it? On the other hand, that was a long time ago, mentally!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment