Robot 3
Working toward a ‘walking skeleton’. I think I know what I’d like it to be.
The “walking skeleton”, to me, is a tiny version of the “whole program”, able to be demonstrated to our primary stakeholders as a sign that we’re on our way. I believe that regularly producing improved versions of the product, and regularly showing them to our stakeholders, is the best way to ensure their continued support, and the best way to be sure that what we’re doing tracks with their wishes.
Now, except in the rare case where one of you writes to me and asks about some feature, I am my own stakeholders, and as such, I always know just exactly how things are going, and so I sometimes go further than I would recommend without creating anything that the stakeholders would see as new.
That’s what lets me do a series of consecutive articles on some difficult refactoring, or to step aside to work on a bit of toolwork, or even to write about entirely different things. In a real product effort, we would have to juggle those things differently. But here and now, I do want to get quickly to something we can call a walking skeleton. It goes something like this:
Walking Skeleton
I’d like to have three components to my initial WS:
- World
- I’d like to have a world containing at least one obstacle, and perhaps some other things;
- Robot
- I’d like to have a robot in that world. I’d like him to be able to scan and “see” the obstacle. I’d like him to be able to move in at least one direction, to show that what he sees varies by position.
- On Screen
- And I’d like there to be a simple screen view showing a rudimentary player’s view of the game.
Now this is a lot. Recall that Hill’s walking skeleton was one message passing across between tasks. No robots, no world, none of that. That’s because he considered the world-robot communication path to be the key component for his walking skeleton. I am finessing that question, for better or worse, and focusing on game play.
Nonetheless, this is a lot. A smaller WS, in my robot world, might be a program that placed a robot and reported its location (I’ve learned that they do know their location). Or we might do a scan from a pre-placed robot and get back a scan reply. We might display all that in text.
How do we decide? We don’t exactly decide. Instead, we keep in mind some near-in date for demonstrating the product, and we put into it just enough capability to ensure that we have something good enough to demonstrate on that date.
And then, we try to have something to demonstrate every single day between now and that date, even if some of the demonstrations would require a lot of explanation.
Yesterday, we could demonstrate, with our tests, that we can place rectangular obstacles in the world, and that the world will not place obstacles that don’t make sense. In prior days, with our tests, we could demonstrate that the robot retains knowledge of what she has scanned, and that she can request a scan from the world and deal with what comes back.
And so on. Our demonstrations, right now, would require us to explain our tests, but we could do that. What would really wow the stakeholders, I think, would be a demo like this:
- We start the program. It displays a grid of some kind, with a square or triangle representing a robot. We say “Here, we have just arrived in World. We have no idea what is around us. We initiate a scan.”
- We press a key or an on-screen button and a couple of things appear on the horizontal or vertical lines around us. “Our scan has seen two obstacles. Recall that scans only go directly north, south, east, and west of us, and just a few squares. Now we move.”
- We press another key or button and the screen adjusts. “We’ve moved forward. The obstacles are still shown, but they are in different positions relative to us, because we have moved. The robot’s memory recalls where things are. Now we scan again.”
- We press the scan button again, and another obstacle or two appear. Perhaps one adjacent to the one we just passed, and a new one on the other side. “Looks like the obstacle to our left is large, and now we see another one to the right.”
For extra credit we’d implement turning and going backward. We might be able to have more than one kind of obstacle, so we could say “The O indicates that it’s a generic obstacle, but the P to our right is a pit! We have to avoid those, as they can be fatal to our mission”
So, today, that’s where I’m heading, toward the ability to show and tell a little story like the one above. I should emphasize “like”. It’s still to early in the effort to know exactly what we can do in a few more days, but we’ll be moving toward that kind of story.
Now that we’ve inspired ourselves, let’s look at the code and see what we want to do today, moving toward our story.
Next Steps
Yesterday, we made these tests run:
_:test("place and detect one obstacle", function()
local world = World(25,25)
world:addFact(-2,10,"O")
_:expect(world:factAt(-2,10)).is("O")
end)
_:test("rectangular obstacle", function()
local world = World(25,25) -- -12 to 12, thanks guys
world:createObstacle(-2,10, 3,8)
for x = -2,3 do
for y = 8,10 do
_:expect(world:factAt(x,y)).is("O")
end
end
end)
_:test("erroneous obstacle calls", function()
local world = World(5,5) -- -2 to 2 thanks guys
world:createObstacle(1,1,2,2)
checkWorldEmpty(world, "not top left")
world = World(5,5)
world:createObstacle(-5,6, 5,-6)
checkWorldEmpty(world, "outside limits")
end)
We placed a single obstacle. Then we placed a rectangular batch of obstacles, because that’s part of the spec. And, finally, we ensured that erroneous attempts to place things are harmless.
Today, now that I’m here, I remember that I wanted to place something other than an obstacle. I want to do that because I think it’ll drive out some interesting code, and because having at least two different kinds of things in the world will make the demo more compelling. And, I think we have plenty of time. Let’s shoot for a demonstrable walking skeleton by next Friday. Today is Saturday.
Let’s write a test that places a rectangular pit, much like the one that places a rectangular obstacle.
_:test("rectangular pit", function()
local world = World(25,25) -- -12 to 12, thanks guys
world:createPit(-2,10, 3,8)
for x = -2,3 do
for y = 8,10 do
_:expect(world:factAt(x,y)).is("P")
end
end
end)
Note that I’ve decided that pits will return a letter “P” to factAt
. You can probably figure out why I’ve chosen that letter. And you may think that we may need something more robust in future, and you may be right. But for now, we’re going to do this. Let’s look at the code for createObstacle
:
function World:createObstacle(left, top, right, bottom)
if self:isValidRectangle(left,top,right,bottom) then
self:addObstacles(left, top, right, bottom)
end
end
function World:isValidRectangle(left,top,right,bottom)
local function inOrder(a,b,c,d)
return a<=b and b<=c and c<=d
end
return inOrder(self:left(),left,right,self:right()) and
inOrder(self:bottom(),bottom, top, self:top())
end
function World:addObstacles(left, top, right, bottom)
for x = left,right do
for y = bottom,top do
self:addObstacle(x,y)
end
end
end
function World:addObstacle(x,y)
self:addFact(x,y,"O")
end
We could, rather obviously, just copy and paste this code, and edit the word “Obstacle” to “Pit”, and be good to go. And that is one way to start. We just have to be sure not to stop there. There would be lots of duplication if we were to do that, and removing duplication is one of the most important ways of evolving toward better code. Why? Because duplication often represents the same idea expressed twice, and it is better to express each idea just once, if we possibly can.
Tell you what: let’s do it that way. I had in mind something more clever, but clever isn’t clever, so let’s do this by duplicating code and then reducing duplication.
However, I’m not going to do it by duplicating that whole raft all at once. I’m going to let my test drive me each step of the way.
Testing, I expect not to find createPit
:
12: rectangular pit -- TestWorld:166:
attempt to call a nil value (method 'createPit')
Right. Copy, paste, rename:
function World:createPit(left, top, right, bottom)
if self:isValidRectangle(left,top,right,bottom) then
self:addPits(left, top, right, bottom)
end
end
I expect to need addPits
:
12: rectangular pit -- TestWorld:32:
attempt to call a nil value (method 'addPits')
Copy, paste, rename:
function World:addPits(left, top, right, bottom)
for x = left,right do
for y = bottom,top do
self:addPit(x,y)
end
end
end
I expect to need addPit
:
12: rectangular pit -- TestWorld:55:
attempt to call a nil value (method 'addPit')
Copy, paste, rename:
function World:addPit(x,y)
self:addFact(x,y,"P")
end
I expect my test to run. It does. Now there is profound duplication. Let’s remove it. We’ll start at the bottom, with these two:
function World:addObstacle(x,y)
self:addFact(x,y,"O")
end
function World:addPit(x,y)
self:addFact(x,y,"P")
end
These are the same except for O and P. Let’s extract the common bits into a new method, addThing
, giving:
function World:addObstacle(x,y)
self:addThing(x,y,"O")
end
function World:addPit(x,y)
self:addThing(x,y,"P")
end
function World:addThing(x,y,thing)
self:addFact(x,y,thing)
end
Tests should run. They do. Now let’s find all the places where addPit or addObstacle are called, and replace them with addThing calls:
function World:addObstacles(left, top, right, bottom)
for x = left,right do
for y = bottom,top do
self:addThing(x,y, "O")
end
end
end
function World:addPits(left, top, right, bottom)
for x = left,right do
for y = bottom,top do
self:addThing(x,y, "P")
end
end
end
Tests should run. They do. Remove addObstacle
and addPit
. Tests still run. We could commit, and if I weren’t sure this was going to work, I would. We now have some more obvious duplication:
function World:addObstacles(left, top, right, bottom)
for x = left,right do
for y = bottom,top do
self:addThing(x,y, "O")
end
end
end
function World:addPits(left, top, right, bottom)
for x = left,right do
for y = bottom,top do
self:addThing(x,y, "P")
end
end
end
Again the only difference is the O or P. We’ll do the same thing, in one step this time: create addThings
(plural), and call it from whoever is calling these two.
function World:addThings(left, top, right, bottom, thing)
for x = left,right do
for y = bottom,top do
self:addThing(x,y, thing)
end
end
end
We find our callers and change them:
function World:createObstacle(left, top, right, bottom)
if self:isValidRectangle(left,top,right,bottom) then
self:addThings(left, top, right, bottom, "O")
end
end
function World:createPit(left, top, right, bottom)
if self:isValidRectangle(left,top,right,bottom) then
self:addThings(left, top, right, bottom, "P")
end
end
Tests should run. They do. We still have some duplication:
function World:createObstacle(left, top, right, bottom)
if self:isValidRectangle(left,top,right,bottom) then
self:addThings(left, top, right, bottom, "O")
end
end
function World:createPit(left, top, right, bottom)
if self:isValidRectangle(left,top,right,bottom) then
self:addThings(left, top, right, bottom, "P")
end
end
Extract the insides, make a method createThings
out of it and call it:
function World:createObstacle(left, top, right, bottom)
self:createThings(left, top, right, bottom, "O")
end
function World:createPit(left, top, right, bottom)
self:createThings(left, top, right, bottom, "P")
end
function World:createThings(left, top, right, bottom, thing)
if self:isValidRectangle(left,top,right,bottom) then
self:addThings(left, top, right, bottom, thing)
end
end
I expect this to pass the tests. It does. Let’s see what we’ve got now:
function World:createObstacle(left, top, right, bottom)
self:createThings(left, top, right, bottom, "O")
end
function World:createPit(left, top, right, bottom)
self:createThings(left, top, right, bottom, "P")
end
function World:createThings(left, top, right, bottom, thing)
if self:isValidRectangle(left,top,right,bottom) then
self:addThings(left, top, right, bottom, thing)
end
end
function World:isValidRectangle(left,top,right,bottom)
local function inOrder(a,b,c,d)
return a<=b and b<=c and c<=d
end
return inOrder(self:left(),left,right,self:right()) and
inOrder(self:bottom(),bottom, top, self:top())
end
function World:addThings(left, top, right, bottom, thing)
for x = left,right do
for y = bottom,top do
self:addThing(x,y, thing)
end
end
end
function World:addThing(x,y,thing)
self:addFact(x,y,thing)
end
function World:addFact(x,y,content)
self._knowledge:addFact(Fact(x,y,content))
end
This is pretty fine. I note that we could get rid of addThing
if we were to call addFact
directly. I think that’s righteous, so let’s do it.
function World:addThings(left, top, right, bottom, thing)
for x = left,right do
for y = bottom,top do
self:addFact(x,y, thing)
end
end
end
Test. Still green. Commit: Can add Pits to World. Common code with Obstacle.
Let’s reflect.
Reflection
If you’re an old hand at this, you might have seen this coming, and you might well have just begun by putting a parameter into createObstacle` at the top, and generalized your way down. And that’s certainly OK, I might do it myself on a given day.
What I continue to find fascinating, however, after Lo! these many years, is how nice and tidy a bottom-up removal of duplication is, and how it leads us right to the top, removing duplication all the way up. And the steps are so easy that even I was able to do them without mistakes (And without those powerful refactoring browsers you kids have these days. Codea is old-school.)
The nice thing about today’s approach, I would suggest, is that even a relatively new hand can do it, because going bottom-up, you just kind of follow your nose, while going top down there is—I think—a bit more of an experienced hand needed, in order to see forward to the end. Maybe not. Maybe they’re equally easy.
Either way, once again we see the big wins that can come from simple removal of duplication.
I do a little tidying, and commit again: Tidying.
You know what? This is about the right amount for an article, and it’s Saturday. Let’s wrap up and enjoy the day.
Summary
Today, we see the power of duplication removal, and the value of tests. If we didn’t have those nice tests for Obstacle and Pit, we’d have been taking a big risk doing that much refactoring. We’d probably have deferred it, just because we couldn’t be confident that we hadn’t broken anything. Or, we might have broken something. The easiest thing to break would have been to fail to change an O to a P somewhere, which without tests would always seem to work but be wrong.
I take a certain pleasure in doing these seemingly mindless refactorings, unwinding the code up toward the top, in tiny steps, each of which I can see clearly. There is a special joy in the rhythm of test, change, test, each step improving things a little bit. It’s almost like rocking in time to B. B. King and Ruth Brown singing “Ain’t Nobody’s Business”. Maybe that’s because that’s what I’m doing.
Smooth and easy. Very pleasant. Our work can be pleasant and joyful, if we make it that way.
See you next time here in the world of robots.