Robot 24
I’ve made a mistake. Or I’m about to make one. Or both. Certainly not neither.
Today is Wednesday, and that means that last night was Friday Night Coding, where we did no real coding, but instead mused about the state of the world, the art, and matters large and small. I did take away some key notions, which will come up as we go along.
Quote Investigator finds no originator for this quote that came up last night:
Good judgment comes from experience. Experience comes from bad judgment.
One early and similar remark was made by Oscar Wilde:
Experience is the name that everyone gives to their mistakes.
As much as people offer solutions: Scrum, SOLID, XP, Learn Quantum Physics in 24 Hours, we cannot really bring about good outcomes by lists of things to do … or even lists of things not to do. That’s very true in the world at large: no list of commandments, however long or detailed, can cause people to do only good and no evil. There are too many loopholes even in the commandments of the Deity.
Of course, we here are mostly here to talk about programming, which is a very human process, and one that involves many people working together. I write mostly about the relationship between me and my code, which is very different from the relationship between a pair with me in it and the code, or a group with me in it and the code.
Because cats are terrible pair programmers, I focus mostly on one nominal person, me, and the code. If you were here with me, it would become an entirely different thing. It would be you and me, interacting with each other … and with the code. The you and me part would be far more rich and far more important than our relationship with the code. And that is as it should be.
I don’t mean “as it should be” as in “that’s OK”. I mean that the human relationships and interactions are more important than our relationship and interaction with the code.
I don’t mean “when the coding is over, the people’s relationships will remain and are more important”. Oh, I do hope that’s true, but what I mean is that when multiple people are involved in the creation of a computer program, it is the way that those people work together that is most important to the results of that effort, as seen in the product, its code, its documentation, its whatever it is.
The code is a focus. It’s a thing we are making. Our relationship with it almost gives it a life. We say that the program wants to be one way and not another. We perceive that this object is obsessed with primitives, that that object wants another one to have some feature or to act differently.
How do we become so involved with our material as to begin to give it personality? It’s a mystery, but it’s one that characterizes humanity. The song we’re writing wants to go up at this point. The chair we’re building wants to be more curved here. It’s part of what we do: we become intimate with our work.
That intimacy does not come from following rules, be they Scrum or SOLID. It comes from making mistakes as well as from doing things right. It comes from making the song go too high, from making the barn in the painting too red, as much as it does—more than it does—from making the tune just right or the barn just the right reddish brown.
To the extent that I’m good at programming, about which I do wonder sometimes, I am good because of all the things I’ve tried that didn’t work, much more than the things I’ve done that turned out just right … if any really have. And that’s why I write the way that I do, showing you what programming is really like for me, a continual trying, fitting, adjusting, improving, breaking, destroying, rebuilding, giving up, starting over, digging in the wrong place, putting things where they don’t belong, trying, trying, trying.
It really is like that Beckett quote that I like:
Ever tried. Ever failed. No Matter. Try again. Fail again. Fail better.
And in that light …
A Mistake?
Yesterday, I was working with the request and response structures in the spec for the Robot Worlds game. I decided that I should use the weird dictionary structure that comes back as a response, and went to the point of actually saving the state dictionary as a member variable in my Robot class:
function Robot:init(name,aWorld)
assert(aWorld:is_a(WorldProxy), "expected WorldProxy")
self._world = aWorld
self._status = State(aWorld:launchRobot(name, self))
self._x, self._y = self._status:position()
self._name = name
self.knowledge = Knowledge()
end
Now, once I had saved the state, I immediately pulled out the things I cared about, namely the x and y position. But yesterday, that was a small step. I fully intended to remove the member variables _x
and _y
, and to write accessors on the Robot, and to implement those accessors to refer to the State.
Wouldn’t it be nice if I could settle on whether it’s a state or a status? This confusion is probably a clue that my mind is unsettled on this, and that the code is not reflecting well-settled ideas.
Today, I’m questioning the idea of tying my Robot so tightly to the state/status format. I want to be sure that the Robot code thinks in the terms that are suitable to that part of the problem, being a good robot. While I’m sure that I can represent the knowledge in one form one place and another form somewhere else, today, I’m questioning yesterday.
I’m going to push forward with the idea, for now, just remaining sensitive to whether I feel like I have to jump through my own hoops to make things work. It’ll be OK. Let’s move forward, eliminating the use of the _x
and _y
in favor of accessors.
Removing _x and _y
Here’s one usage:
function Robot:forward(steps)
local x,y = self._world:forward(self._name, steps)
newDx = x - self._x
newDy = y - self._y
self.knowledge = self.knowledge:newLens(newDx,newDy)
self._x = x
self._y = y
end
This one is actually tricky. The issue is that the newLens
method is designed to step, not to set:
function Lens:newLens(dx,dy)
return Lens(self._knowledge, self._x+dx, self._y+dy)
end
This is no longer really appropriate. Let’s begin by extracting a method from newLens
:
function Lens:newLens(dx,dy)
return self:newLensAt(self._x+dx, self._y+dy)
end
function Lens:newLensAt(x,y)
return Lens(self._knowledge, x,y)
end
That’s surely harmless. We’ll test of course. Green. Commit: new method Lens:newLensAt(x,y).
Now let’s use it here:
function Robot:forward(steps)
local x,y = self._world:forward(self._name, steps)
newDx = x - self._x
newDy = y - self._y
self.knowledge = self.knowledge:newLens(newDx,newDy)
self._x = x
self._y = y
end
That can now be this:
function Robot:forward(steps)
local x,y = self._world:forward(self._name, steps)
self.knowledge = self.knowledge:newLensAt(x,y)
self._x = x
self._y = y
end
We’re moving toward getting rid of the member variables but we’re not there yet. Test. Ha, error!
1: Robot updates knowledge on move -- TestRobot:137: attempt to call a nil value (method 'newLensAt')
Right, need that on Knowledge, which has this:
function Knowledge:newLens(x,y)
return Lens(self,x,y)
end
Oh my! I’m glad I started this article talking about mistakes. The water, if indeed it is water, is deeper than I thought.
I’m going to revert both those commits and think again. This is irritating, as I thought I was OK, and I really wanted to do lots of tiny commits today. Oh well, fail better. Undo today’s commits. I’m glad they were small.
The Lens is Relative—Or Is It?
I’m not sure whether we have a serious problem or a naming problem. The issue is somewhere in the range of the robot finding out information relative to itself, and thinking relative to itself, and the world (and our general knowledge of it) being expressed in real world coordinates.
The job of the Lens is to deal with the difference. The Knowledge is stored in actual world coordinates. Something might be at (3,5) in Knowledge. That’s where it really is.
If the robot is at (4,5), that object is at (-1,0) relative to the robot. If the robot turns left, the object will be at (0.1) relative to the robot. Then if the robot steps forward one step, it will land on the object.
The job of the Lens, and the partially implemented RotatingLens, is to allow the robot to think in local terms and to find out things from the big map. “What’s a step to the left?” “The object at (3,5).”
So. The x and y in Lens are the real world coordinates. That’s fine. But we presently create the lens in these ways:
function Robot:forward(steps)
local x,y = self._world:forward(self._name, steps)
newDx = x - self._x
newDy = y - self._y
self.knowledge = self.knowledge:newLens(newDx,newDy)
self._x = x
self._y = y
end
function Lens:newLens(dx,dy)
return Lens(self._knowledge, self._x+dx, self._y+dy)
end
function Knowledge:newLens(x,y)
return Lens(self,x,y)
end
function Lens:init(knowledge, x, y)
self._knowledge = knowledge
self._x = x
self._y = y
end
The above looks wrong to me. I think it just happens to work because, so far, our robot starts at 0,0. (It might continue to work, by accident. It’s not clear to me.)
The fact is, the Lens is supposed to have real coordinates in it. Let’s try something. We’ll make newLens
stop doing the addition and our call to it stop doing the subtraction. That should “just work”.
function Lens:newLens(x,y)
return Lens(self._knowledge, x,y)
end
Now we have to fix all the calls to be sure they’re doing what we want. The main one, we’ve already seen:
function Robot:forward(steps)
local x,y = self._world:forward(self._name, steps)
newDx = x - self._x
newDy = y - self._y
self.knowledge = self.knowledge:newLens(newDx,newDy)
self._x = x
self._y = y
end
Easy fix here:
function Robot:forward(steps)
local x,y = self._world:forward(self._name, steps)
self.knowledge = self.knowledge:newLens(x,y)
self._x = x
self._y = y
end
Before I look at all the tests, I’m going to run them.Good news. Two fail and the game works properly. The fails are:
3: Knowledge adjusted twice --
Actual: A,
Expected: B
3: Knowledge adjusted twice --
Actual: nil,
Expected: A
The test is:
_:test("Knowledge adjusted twice", function()
local knowledge = Knowledge()
knowledge:addFactAt("A",1,1)
local lens = knowledge:newLens(2,2)
_:expect(lens:factAt(-1,-1)).is("A")
lens:addFactAt("B",1,1)
local lens2 = lens:newLens(1,1)
_:expect(lens2:factAt(0,0)).is("B")
_:expect(lens2:factAt(-2,-2)).is("A")
end)
I expect that the errors are the last two expects. The code is assuming addition in newLens, so the second setting should now be (3,3). Change it:
_:test("Knowledge adjusted twice", function()
local knowledge = Knowledge()
knowledge:addFactAt("A",1,1)
local lens = knowledge:newLens(2,2)
_:expect(lens:factAt(-1,-1)).is("A")
lens:addFactAt("B",1,1)
local lens2 = lens:newLens(2+1,2+1)
_:expect(lens2:factAt(0,0)).is("B")
_:expect(lens2:factAt(-2,-2)).is("A")
end)
I wrote “2+1” to indicate that the adjustment is by (1,1), rather than just saying (3,3). Possibly better.
We are green and we have actually accomplished something. We’re no longer adjusting lens coordinates by dx and dy. I want the code to better reflect that. Let’s change the method name to newLensAt
, signifying that we’re passing in the coordinates, not the change. We can do another method should we ever need it.
Done, tests green. Commit: Method newLens is NewLensAt, throughout, always takes world coordinates not dx dy.
What Were We Doing?
Oh, right, we were trying to get rid of the _x
and _y
in Robot. Are we ready to do that yet? We do have an issue in the method we’ve been focused on:
function Robot:forward(steps)
local x,y = self._world:forward(self._name, steps)
self.knowledge = self.knowledge:newLensAt(x,y)
self._x = x
self._y = y
end
Things would be better if the forward
function were returning a state or status or whatever it’s called. Let’s see what other uses we have of those members. Good news! There are none. Let’s extract a method:
function Robot:forward(steps)
local x,y = self._world:forward(self._name, steps)
self.knowledge = self.knowledge:newLensAt(x,y)
self:setPosition(x,y)
end
function Robot:setPosition(x,y)
self._status:setPosition(x,y)
end
function State:setPosition(x,y)
self._dict.position = {x,y}
end
I expect green. I don’t quite get it, because:
5: Robot moves forward on 1 key --
Actual: 0,
Expected: 1
What is that test?
_:test("Robot moves forward on 1 key", function()
local world = WorldProxy()
world:addFactAt("O",3,1)
local robot = Robot("Louie", world)
_:expect(robot._y).is(0)
robot:scan()
_:expect(robot:factAt(3,0)).is(nil)
robot:keyboard("1")
_:expect(robot._y).is(1)
robot:scan()
_:expect(robot:factAt(3,0)).is("O")
end)
Ah some nasty stuff there. Improve the test:
_:test("Robot moves forward on 1 key", function()
local world = WorldProxy()
world:addFactAt("O",3,1)
local robot = Robot("Louie", world)
_:expect(robot._y).is(0)
robot:scan()
_:expect(robot:factAt(3,0)).is(nil)
robot:keyboard("1")
_:expect(robot:y()).is(1)
robot:scan()
_:expect(robot:factAt(3,0)).is("O")
end)
That will drive out the y
method:
5: Robot moves forward on 1 key -- TestRobot:68: attempt to call a nil value (method 'y')
And …
function Robot:y()
return self._status:y()
end
And …
function State:y()
return self._dict.position[2]
end
And I’m smart enough to use it:
function State:position()
return self._dict.position[1],self:y()
end
I expect green. I get it. I take the occasion to do x, which I can require simply by using it in position
:
function State:position()
return self:x(),self:y()
end
function State:x()
return self._dict.position[1]
end
Should be green. Green it is. Commit. Implement x() and y() on Robot. Pushes down to status.
Now that word, status. Shouldn’t it be state? I think this is the definitive answer, the format of the Response blob:
function World:launchRobot(name, kind,shieldStrength, shots)
local robot = RobotState(self, name, kind, shieldStrength, shots)
self._robots[name]=robot
local response = {
result="OK",
data={},
state={
position = {robot._x, robot._y},
direction = "N",
status = "NORMAL",
shots = robot._shots,
shields = robot._strength
}
}
return response
end
The big thing is state
, the small one is status
. So the references we have should mostly be state not status. Let’s get the names right: it will help get our minds right.
-- state object local to World side.
local RobotState = class()
function Robot:init(name,aWorld)
assert(aWorld:is_a(WorldProxy), "expected WorldProxy")
self._world = aWorld
self._state = State(aWorld:launchRobot(name, self))
self._x, self._y = self._state:position()
self._name = name
self.knowledge = Knowledge()
end
function Robot:setPosition(x,y)
self._state:setPosition(x,y)
end
function Robot:y()
return self._state:y()
end
Should be green. Green. Commit: Refer to state as state throughout.
Let’s reflect.
Reflection
OK. I think we’ve sorted out the x and y and making sure that Knowledge and Lens make some sense. We’ve simplified the return from forward
, though we’re not all the way to getting a response thingie back.
Remember that vague pencil sketch I showed yesterday, reflecting my equally vague notion that there will be a series of objects with responsibility to build a request, jsonize it, transmit it, de-jsonize it, execute it, prepare a response object, jsonize it, transmit it, de-jsonize it, and tuck it away in the Robot.
We really only have one real instance of even trying to do that, in the method we call scan
:
function WorldProxy:scan(...)
local jsonString = self.world:scan(...)
local outcome = json.decode(jsonString)
local packets = outcome.data.objects
--print(#packets, " packets")
local result = {}
for i,p in ipairs(packets) do
local lp = LookPacket:fromObject(p)
--print(lp)
table.insert(result, lp)
end
return result
end
function World:jsonLookResult(packetArray)
local outcome = {}
outcome.result="OK"
local objects = {}
for i,p in ipairs(packetArray) do
table.insert(objects,p:asObject())
end
outcome.data = { objects=objects }
local jsonString = json.encode(outcome, {indent=true})
return jsonString
end
And here, we’re not even returning the required state information. We do have another place that does create it:
function World:launchRobot(name, kind,shieldStrength, shots)
local robot = RobotState(self, name, kind, shieldStrength, shots)
self._robots[name]=robot
local response = {
result="OK",
data={},
state={
position = {robot._x, robot._y},
direction = "N",
status = "NORMAL",
shots = robot._shots,
shields = robot._strength
}
}
return response
end
Let’s extract a bit of a method there:
function World:launchRobot(name, kind,shieldStrength, shots)
local robot = RobotState(self, name, kind, shieldStrength, shots)
self._robots[name]=robot
local response = {
result="OK",
data={},
state=self:robotState(robot)
}
return response
end
function World:robotState(robot)
return {
position = {robot._x, robot._y},
direction = "N",
status = "NORMAL",
shots = robot._shots,
shields = robot._strength
}
end
Test expecting green. I doubt anyone actually cares much. Green. Commit: World:robotState() returns robot state dictionary.
Now we can use that here:
function World:forward(robotName,steps)
local robot = self._robots[robotName]
robot._y = robot._y + steps
return robot._x, robot._y
end
We’ll return just the state for now:
function World:forward(robotName,steps)
local robot = self._robots[robotName]
robot._y = robot._y + steps
return self:robotState(robot)
end
Now we need to expect that on the other side:
function Robot:forward(steps)
local x,y = self._world:forward(self._name, steps)
self.knowledge = self.knowledge:newLensAt(x,y)
self:setPosition(x,y)
end
That becomes:
function Robot:forward(steps)
self._state = self._world:forward(self._name, steps)
self.knowledge = self.knowledge:newLensAt(self:x(),self:y())
end
And that requires:
function Robot:x()
return self._state:x()
end
I expect this to work. I am surprised when it does not.
1: Robot updates knowledge on move -- TestRobot:152: attempt to call a nil value (method 'x')
That means that State doesn’t implement x
. I thought it did … and it does:
function State:x()
return self._dict.position[1]
end
function State:y()
return self._dict.position[2]
end
Hm. What evil have I done?
Oh. I didn’t store a State, I stored the dictionary. For now the fix is this:
function Robot:forward(steps)
self._state = State(self._world:forward(self._name, steps))
self.knowledge = self.knowledge:newLensAt(self:x(),self:y())
end
Now green? Yes, fine. Commit: forward command now returns and receives a robot state dictionary. Needs to be a full response object.
No. That’s the wrong place to have done the State. Put that back:
function Robot:forward(steps)
self._state = self._world:forward(self._name, steps)
self.knowledge = self.knowledge:newLensAt(self:x(),self:y())
end
And in WorldProxy:
function WorldProxy:forward(name,steps)
local result
local rq = {
robot=name,
command="forward",
arguments={steps}
}
local jsonRq = json.encode(rq)
local decoded = json.decode(jsonRq)
local dName = decoded.robot
local dCommand = decoded.command
if dCommand == "forward" then
local dSteps = decoded.arguments[1]
return State(self.world:forward(dName,dSteps))
end
assert(false,"impossible command "..dCommand)
end
That’s better. We want the proxy, the start of the communication chain, to do the encoding and decoding. So it should return us a State for now, and probably a Response in due time.
I’m not sure that this is the breakdown we’ll wind up with, but I want the code here in `forward to be expressing most of the steps all together here where we can see them. In fact … this would be better:
if dCommand == "forward" then
local dSteps = decoded.arguments[1]
local dict = self.world:forward(dName,dSteps)
local shouldBeResponse = State(dict)
return shouldBeResponse
end
I’m trying to express there that we’re not done yet.
Test. Green. Commit: move state creation to WorldProxy, in forward.
Time to lift our head again.
Reflection Again
To me it feels like we are edging up on having the Request and Response and State dictionaries and JSONs and objects. We’ve got parts of them in luanch, parts in WorldProxy forward, parts not done at all.
We’re moving in a direction that I think is “roughly good”. We’re in Nebraska, trying to get somewhere in New York that we’re not sure of, but it’s probably a good idea to drive east. If we jog north a bit around Des Moines, it’ll probably be OK. If we hit Sioux City, we may be in trouble.
We don’t follow a straight line from where we are to where we should be. We don’t even know for sure where we should be, and if we did, going straight there will almost always be far too big a bite. Step by step, inch by inch1.
Moving a Bit
Shall we move another inch? Let’s do. These two methods happen to be side by side in my window:
function WorldProxy:forward(name,steps)
local result
local rq = {
robot=name,
command="forward",
arguments={steps}
}
local jsonRq = json.encode(rq)
local decoded = json.decode(jsonRq)
local dName = decoded.robot
local dCommand = decoded.command
if dCommand == "forward" then
local dSteps = decoded.arguments[1]
local dict = self.world:forward(dName,dSteps)
local shouldBeResponse = State(dict)
return shouldBeResponse
end
assert(false,"impossible command "..dCommand)
end
function WorldProxy:launchRobot(...)
local response = self.world:launchRobot(...)
return response.state
end
Launch is getting a response back. Forward is not. We were just looking at that. Let’s gin up a simple response for forward, over in World:
function World:forward(robotName,steps)
local robot = self._robots[robotName]
robot._y = robot._y + steps
local response = {
result = "OK",
data = {},
state = self:robotState(robot)
}
return response
end
And in the proxy, expect that:
function WorldProxy:forward(name,steps)
local result
local rq = {
robot=name,
command="forward",
arguments={steps}
}
local jsonRq = json.encode(rq)
local decoded = json.decode(jsonRq)
local dName = decoded.robot
local dCommand = decoded.command
if dCommand == "forward" then
local dSteps = decoded.arguments[1]
local responseDict = self.world:forward(dName,dSteps)
return State(responseDict.state)
end
assert(false,"impossible command "..dCommand)
end
OK, that’s a bit closer to what we want. In both launch and forward, we’re getting a response dictionary back and pulling the state out.
Commit: refactor a bit closer to having response objects.
Lets reflect and sum up one more time, and get outa here.
Reflecto-Summary
The creation of a command, a response, and the subset of response that is a state is beginning to take shape. It’s ind of like coming into shore in the fog. Things begin to appear, at first big and vague, but taking shape as we get closer. It would be nice if programming were all crisp lines and just right, but I don’t think that happens often.
We try to make it happen. We tell ourselves what other people have told us, that if we’d just do more analysis, more design, more diagrams, more meetings, we’d know exactly what to do and then do it. Well, I’m sorry, Bunkie, but that’s just now how it is. They don’t know what they want, much less what they need, they say things they don’t mean, they change their minds, we don’t understand them, and what we do understand we don’t quite know how to do, and when we type it in, the #@$%$! doesn’t quite work.
That’s how it is when you’re creating, and this, my young padawan, and my old master, is creation. It’s messy. We make a mess, we clean it up, we make another mess. We try not to let it get too messy, because when that happens we slow down and get lost. But we don’t expect a crisp line from Nebraska to Park Avenue. It doesn’t work that way, and Park Avenue isn’t where we wind up wanting to go anyway. Maybe somewhere near Boston …
But notice … NOTICE! … the program is getting better. Things are moving to better places. Names better reflect our understanding. The tests keep running and when they break it’s for a good reason.
It’s getting better.
Ever tried. Ever failed. No matter. Try again. Fail again. Fail Better.
Come fail with me again tomorrow!
-
Niagara Falls! ↩