Plotters :: Pantsers
In writing, folx refer to Plotters, who plan out the story in advance, and Pantsers, who “fly by the seat of their pants”. Which am I? Which are you?
There can be little doubt that I am a pantser, at least much of the time here in these articles. I have a good general idea of what the story should be, and I spend what must seem to be all my time changing little things here and there. I would think you might find it difficult to see where I’m going, why I’m doing whatever I seem to be doing. even though I do try to describe what I’m about.
There are a few reasons why I work that way. They include:
- End to End
- I suppose that if I were writing a fairy tale, I would first write “Once upon a time, they lived happily ever after”, and then start filling in the middle. In software, my long experience has caused me to believe that, faced with a large long-term effort with wide-ranging goals, I would try to get an end-to-end version running, with the most important features, as soon as possible.
-
So in these articles, I try to give examples of a program growing, not from one end to the other, but instead growing on the inside, but always keeping the whole thing as visible as possible.
- Legacy Code
- Since we are “in the business of changing code”, as GeePaw Hill puts it, programmers who work for a living are faced, every day, with code written in the past by themselves or others, less experienced than they are now, often under great pressure. By the nature of incremental end-to-end development, and simply because we cannot write it all at once, programmers face a lot of code that isn’t as good as it needs to be, and that needs to be changed to be better.
-
So in these articles, I often go back and find code that needs improvement, and show how it can be made better, more flexible, and more capable, without tearing down the house and starting over.
- Fun
- I enjoy writing and changing code. I love taking some code that needs improvement and improving it. It pleases me to perform simple manipulations on the code and seeing them produce better code. I wish that all programmers could enjoy working with programs as much as I do.
-
So in these articles, I do as I do. I show you how I create the code, I go back later and discover its needs, I improve it. Often I succeed at that; sometimes I fail. That is the nature of a craft, and for that matter, the nature of science and, if you think that way, the nature of every profession. No one, no set of rules, creates perfection. What we can do, is add some improvement.
But all that doesn’t mean that I do no plotting, and I’d bet that if we could read the minds of most pantser fiction writers, we’d find them musing about plot points, obstacles that might arise, gambits that Just Might Work, and so on. I do not plot my program from beginning to end, but I do often create a tentative plot for a series of changes that I plan (!) to make.
Frequent readers will note that I do often start these articles with a plot, a plan, and then … something else happens. Kent Beck used to recommend that our code should be allowed to take part in our planning, and he was right, because the code is rarely just like we remembered it, the need is rarely just exactly what we imagined, and sometimes there is a giant pit in the way that we have to either move around or fill in before we can do what we planned.
This is the nature of real software development.
So what is the right balance between Plotter and Pantser in your software development? No one can know—not even you. If I were one to make recommendations—and you know I’m not—what I would recommend might be to try to notice when you’ve plotted too much, if you lean toward Plotter, and to try to push more in the direction of being guided by the actual code. If you’re more of a Pantser at heart, try to notice when your free-wheeling gets you in trouble, and try pushing more in the direction of plotting.
The idea would be to begin to build up a sense of how much planning is best in your situation, and how much just finding your way through the weeds is best.
But I make no recommendations, so you’ll have to figure out for yourself what to do.
Let’s do a little plotting and a little pantsing.
What Shall We Do Now?
- Plotter
- We are nearly far enough along with structuring for client-server, I believe. We’ll keep an eye on it to avoid any accidental connections between the sides, but I believe the isolation is good enough.
-
I do think we should package up all the commands that a Bot wants to issue and execute them all at once, instead of one at a time, because that’s more like what the real program will need to do.
-
We need some kind of Bot collection on the client side, containing all the bots and giving them their turns to decide what to do. One thing that we don’t know yet is how the sequencing between client and server will work. If there’s only one client, running lots of bots, that’s one thing. But we had envisioned, at one time, a server running multiple populations of bots in the very same world. That probably implies some kind of turn-taking logic.
-
I’d like to give the Bots more complex problems to solve and smarter behavior. Perhaps even more than one kind of Bot. One possibility is that the Bots need food or fuel and when low on fuel they stop looking for blocks and start looking for fuel. Perhaps if they have encountered fuel, they remember roughly where it is. Or perhaps they leave traces where they walk, pheromones as it were, and tend to follow pheromone trails. Could they become more efficient workers via such a simple idea?
- Pantser
- Yesterday, we improved the
drop
logic, I think it was. We should look atstep
andtake
to see if they need improvement as well. We pushed some intelligence over to Map, so afterstep
andtake
, Map may need a look. (See me plotting right in the middle of pantsing? That’s how you do that.) -
And, of course, while we do that, we’ll look to see what else needs doing. Let’s get to it.
The Code Joins Us
We are, of course, green. Here are step
, take
, and drop
.
class World:
def step(self, bot):
location = self.bots_next_location(bot)
self.map.attempt_move(bot.id, location) # changes world version
real_bot = self.map.at_id(bot.id)
self.set_bot_vision(real_bot)
self.set_bot_scent(real_bot)
def take_forward(self, bot: Bot):
take_location = self.bots_next_location(bot)
if take_location == bot.location:
return
entity = self.map.at_xy(take_location.x, take_location.y)
if self.can_take_entity(entity):
self.map.remove(entity.id)
bot.receive(entity)
def drop_forward(self, bot, entity):
drop_location = bot.location + bot.direction
if self.map.place_at(entity, drop_location):
bot.remove(entity)
- Note
- I was speaking above about letting the code participate in the design. I keep talking about
step
,take
, anddrop
but in fact the methods aretake_forward
anddrop_forward
. A small but important difference.
One might think that taking and dropping would be much the same and yet they seem to be rather different. The dropping code just defines the target location and asks the map whether it was able to place the entity there. If it did, then it removes it from the bot’s inventory. Rather straightforward. Why is taking not much the same?
There is at least one difference that needs to be considered: we do not allow the Bot to take another Bot, so the code will need to include that notion. What’s in that can_take_entity
code in World?
def can_take_entity(self, entity):
return entity and entity.name != 'R'
I am coming up with a clever idea here. On the surface, it appears that we will have to ask the Map whether there is something to take at the provided location, and then, depending on what it is, tell the map to remove it, and give it to the client bot.
What would take look like it Map was really helpful? Perhaps like this:
def take_forward(self, bot):
take_location = bot.location + bot.direction
if (block := self.map.take_block_at(location)):
bot.receive(block)
But Map doesn’t know anything about the types of what it stores, so we really probably don’t want to push entity knowledge down into it.
What about this:
def take_forward(self, bot):
def is_block(entity):
return entity.name == 'B'
take_location = bot.location + bot.direction
if block := self.map.take_conditionally_at(take_location, is_block):
bot.receive(block)
Here,, we ask the Map to return what’s at the location, given that it passes our provided condition. (And, as we’ll see in a moment, if it exists.)
So in Map:
def take_conditionally_at(self, take_location, condition):
item = self.at_xy(take_location.x, take_location.y)
if item and condition(item):
self.remove(item.id)
return item
else:
return None
If there’s an item and it matches the condition, we remove it (using its id) and return it. Otherwise we return None.
The tests are green. Commit: implement and use take_conditionally_at
in take_forward
.
Reflection
It would not surprise me if that code in take_foward
is the first time we’ve ever passed a function to another function (method) in production. I am pretty sure we do it in tests. A quick search for ‘def.*\n.*def’ informs me that, yes we have used the idea in tests but not in production.
Is this too deep in the bag of tricks? I don’t think so, but then I just pulled it out of the bag of tricks, didn’t I? We have done this sort of thing all the time in Kotlin, where you pass lambdas all over when creating things. We have not used the explicit lambda in this app at all. We could, though:
def take_forward(self, bot):
take_location = bot.location + bot.direction
if block := self.map.take_conditionally_at(take_location,
lambda e: e.name == 'B'):
bot.receive(block)
Either way works. I think the explicit function is better. Here’s a third way:
def take_forward(self, bot):
take_location = bot.location + bot.direction
is_block = lambda e: e.name == 'B'
if block := self.map.take_conditionally_at(take_location, is_block):
bot.receive(block)
You pays yer money and you takes yer choice, I guess. I think we’ll stick with the def
, though I confess I rather like the last version. The embedded lambda is too hard to read but that final version looks simpler than the first. Compare:
def take_forward(self, bot):
take_location = bot.location + bot.direction
is_block = lambda e: e.name == 'B'
if block := self.map.take_conditionally_at(take_location, is_block):
bot.receive(block)
def take_forward(self, bot):
def is_block(entity):
return entity.name == 'B'
take_location = bot.location + bot.direction
if block := self.map.take_conditionally_at(take_location, is_block):
bot.receive(block)
See what I mean? I’ve convinced myself to go with the third way. Commit: use lambda to replace nested def.
Reflection++
What do we see now? Well, we see that this is a really weak way to decide whether an entity is a block or not. That tells us that the entity / block / bot concept is not clear enough on the World side. (Probably not on the Bot side either, but they should be separate and we’re not looking over there right now.)
The World will presumably support many kinds of entities, so we really do have a need to begin to build up some kind of sensible hierarchy. I’ve made a card for it.
It seems likely to me that what we’ve done here will allow us to improve step
as well, but we have accomplished enough for today. Let’s sum up.
Summary
We’ve seen a bit of how one programmer balances planning and doing, or plotting and pantsing if you’ll permit those terms. And we’ve seen that even when apparently pantsing, there’s a lot of plotting that goes on. I don’t know what other writers do, but as I write, I often pause, think, and occasionally even revise a bit. Just like the code, I often pause, think, try something, revise it, try again.
And we’ve seen the code get better as we do those things. And that is how you do that. See you next time!