Robot 36 — Just Need to Code
I have an idea for something that might be better. But that’s not my reason for being here. (And should all ojbects be written for their own convenience? Yes, and …)
It’s only about 0700 and I just had to get up. I was thinking about the world and where it’s going. I was trying to imagine writing some fiction, which I’ve always sort of wanted to do—shut up, agile haters—and every idea I had turned dystopian within minutes. So I’m going to code, hoping that my program doesn’t turn dystopian.
Although—the current spec for the robots is a bit dystopian all on its own. We need a better robot game, one that is cooperative and building something rather than where our mission is apparently to be the last robot standing.
We’re working on that, slowly. Today, I just want to work some code. If I were a potter, I’d just want to get my hands in the clay this morning. My clay is code. Which is nice: my hands stay relatively clean.
Requests, not Method Calls
When I started building this program, I thought that the model of Robot calling methods on the World was a sensible design. In fact, I still think it’s a sensible design. As a rule, I find that objects work best when they are fashioned around collaboration, with methods and returns that make sense to the objects we have.
The spec for this program, and for many such programs, is a client-server kind of thing, where the client sends messages to the server, and the server sends messages back, and the messages themselves are defined in some form that no one really likes. In our case they are structured objects, dictionaries, with named elements containing values, or other dictionaries, all coming down in the end to the parameters we’d like to just be passing in, and the objects we’d like to get back.
When those two ideas, methods and returns, and formatted messages, come together (which happens to be playing right now on the Beatles channel), the best thing I know is to build an adapter object that translates what I’d like to say into what I have to say, and translates what comes back into what I wish would come back.
In the current program, most of that is going on in the WorldProxy. We could call it WorldAdapter if we wanted to name it after the pattern.
Current Code
When I started out this morning, I was thinking of the code we have that looks like this in the Robot cllient:
function Robot:keyboard(key)
if key == "l" then
self:look() -- look
elseif key == "w" then
self:forward(1)
elseif key == "s" then
self:back(1)
elseif key == "d" then
self:turn("right")
elseif key == "a" then
self:turn("left")
end
end
function Robot:look()
local callback = function(response) self:lookCallback(response) end
self._world:look(self._name,callback)
end
function Robot:lookCallback(response)
local packets = LookPacket:fromResponse(response)
for i,packet in ipairs(packets) do
self:addLook(packet)
end
self:standardResponse(response)
end
And like this in the WorldProxy:
function WorldProxy:look(name, callback)
local request = {
robot=name,
command="look",
arguments={}
}
local responseDict = self.world:request(request)
callback(Response(responseDict))
end
I was thinking what I now believe (would you believe the channel just played “I believe in yesterday” from “Yesterday”?) … I was thinking what I now believe was a mistake, along the lines of
Well, we’re always building these requests, maybe we should create them in Robot.
No, I think that’s mistaken. The Robot should be written for its own convenience, as should every object. We can find some improvements to what we have, but pushing the request over into Robot would not be an improvement by my standards.
Do I really mean that? Shouldn’t an object be written for the convenience of its users? Yes, that’s true as well, but internally, the object should be clean and simple, and it should be doing easy things. If it’s complex, there’s a good chance that it needs help from other objects. When we work this all the way down, we tend to get lots of small objects, and almost all of them are simple, clear, and do things that are quite straightforward.
Now yesterday I actually moved the LookPacket code from WorldProxy back over to Robot. I think that’s a mistake. Look at that callback again:
function Robot:lookCallback(response)
local packets = LookPacket:fromResponse(response)
for i,packet in ipairs(packets) do
self:addLook(packet)
end
self:standardResponse(response)
end
There’s something that I don’t like about that … the code wants a collection of LookPackets, because that’s what it thinks would be nice to have. So that’s fine. But the code also expresses that the response doesn’t provide the LookPackets … instead LookPackets have to create themselves by trolling through the Response somehow.
Now that is in fact the case: we do need to troll through the response a bit. But we don’t have to stare right at that ugly bit of reality. What I’d like is for that method to look more like this (and I’m going to make that happen right now):
function Robot:lookCallback(response)
self:updateMap(response)
self:standardResponse(response)
end
function Robot:updateMap(response)
local packets = LookPacket:fromResponse(response)
for i,packet in ipairs(packets) do
self:addLook(packet)
end
end
OK, that’s better. Composed Method pattern on the lookCallback
. Test. Green. Commit: Apply Composed Method to Robot:lookCallback. Pull out updateMap.
But I still don’t think that we should have to make those packets. We wish that the response just had them. We want to say this:
function Robot:updateMap(response)
for i,packet in response:lookPackets() do
self:addLook(packet)
end
end
Well, I just typed that in. It won’t work, but if Response knew lookPackets …
function Response:lookPackets()
return ipairs(LookPacket:fromResponse(self))
end
I rather think that’s going to work. And it does. Green. Commit: Robot updateMap uses Response:lookPackets method.
OK, let’s look at that as a whole:
function Robot:look()
local callback = function(response) self:lookCallback(response) end
self._world:look(self._name,callback)
end
function Robot:lookCallback(response)
self:updateMap(response)
self:standardResponse(response)
end
function Robot:updateMap(response)
for i,packet in response:lookPackets() do
self:addLook(packet)
end
end
function Response:lookPackets()
return ipairs(LookPacket:fromResponse(self))
end
What’s not to like? Well, I don’t like the i
being there. We could give it an _ name to signify that it doesn’t matter. We could build a special iterator that just returns the packets. We could pass a function to Response and let it do the work. That would look something like this:
function Robot:updateMap(response)
local add = function(packet) self:addLook(packet) end
response:looksDo(add)
end
If the function notation in Lua was more compact, I might prefer that. As it is, it’s not more clear. What about this format:
function Robot:updateMap(response)
response:looksDo(function(packet) self:addLook(packet) end)
end
Well, maybe. I’ll allow it. Let’s do the looksDo
method.
function Response:looksDo(f)
for i,p in self:lookPackets() do
f(p)
end
end
Test. Green. Commit: Refactor updateMap to use WP:looksDo
.
Let’s reflect.
Reflection
We now have this in Robot:
function Robot:lookCallback(response)
self:updateMap(response)
self:standardResponse(response)
end
function Robot:updateMap(response)
response:looksDo(function(packet) self:addLook(packet) end)
end
We used to have this:
function Robot:lookCallback(response)
local packets = LookPacket:fromResponse(response)
for i,packet in ipairs(packets) do
self:addLook(packet)
end
self:standardResponse(response)
end
Better? I think so. The Robot still gets to say what it wants to do about looks, but it doesn’t have to know where they come from or how they’re created. That seems to me to be a better arrangement. I’d like it even better if passing in a function parameter was a bit less clunky, but that’s not bad at all. If we did more of it, we’d become more and more used to it and would probably like it even better.
I’m less than an hour in. Let’s sum up, publish this, and then maybe do more.
Mini-Summary
Focusing for just a bit on a small bump in the code has resulted in simplified Robot code, and the Response code isn’t bad either:
function Response:looksDo(f)
for i,p in self:lookPackets() do
f(p)
end
end
function Response:lookPackets()
return ipairs(LookPacket:fromResponse(self))
end
A nice little improvement, and something to think about for other loops in the future.
The larger lesson has to do with “feature envy”, the tendency to write code that pulls bits out of other objects and processes them. It’s usually better to ask those other objects to do more of the work, as we did here. When I’m writing in Codea Lua, I seem often to do that sort of thing. I blame my long experience in procedural languages.
Except that I don’t blame anything or anyone. I just try to see how I can have a bit more fun by doing things a bit better. To me, that’s where the joy of programming can be found
And it appears I’ll never run out of ways to improve, really simple ways at that.
Join me next time!