P-283: Intention
Python Asteroids+Invaders on GitHub
I want to talk today about Programming by Intention, a notion I learned from Kent Beck and Ward Cunningham.
I had occasion the other day, in another world, to use Programming by Intention. It worked every so nicely, and so I want to write about it here. The trick will be finding a place for it.
“What even is it?”, you may be asking yourself. Well, I have written about it before, like here. Maybe you should just go read that: it has the advantage of being quite brief. There is a longer example here. Maybe you should go read that: it has the advantage of being, well, not here. It has the possible disadvantage of being in Codea Lua, but that won’t give you any trouble.
Here, I want to draw an analogy that I think is of some value, but to do that we need some code and some understanding.
What is “Programming by Intention”? If you come from a certain era and region, you may be familiar with the idea of “pseudocode”, where you write down your algorithm in a sort of code-like form, but in no particular language. The idea is to suss out the shape of the algorithm at a high level. Then, after you have it is a few lines of pseudocode, you refine each of those lines, like a function, generally in more pseudocode. Rinse, repeat, and pretty soon you have figured out your algorithm and can write the real code.
Programming by Intention is just like that, except that you do it with real code. What this usually comes down to is expressing what needs to be done with a few high-level calls to functions or methods that don’t yet exist. Then, step by step, inch by inch, slowly we turn the missing functions into real functions, and when the last one is done, the algorithm is programmed1.
In a moment, I’ll try to come up with an example in our current programming universe. But roll with me for now.
There is another application for the word “intention” that we use all the time around here. We always want our code to express our intention, our ideas about what the code does, not just how it does it. We often improve the code’s expressive ness by extracting a method and giving it an Intention-Revealing Name2.
I have found an example of some code in Invaders that isn’t as expressive as it might be. How do I know? Because I was just reading it and I had to think more than I would have liked to. It’s in the PlayerMaker class. Let’s have a look.
We know roughly what the PlayerMaker does: It sits in the mix and if the player gets killed by an invader missile, the PlayerMaker is supposed to notice this and, if there is a player left in the reserves, bring in a new player after a couple of seconds delay.
The code looks like this:
class PlayerMaker(InvadersFlyer):
def __init__(self):
self.reserve = None
self.player_missing = True
def begin_interactions(self, _fleets):
self.reserve = None
self.player_missing = True
def interact_with_invaderplayer(self, _player, _fleets):
self.player_missing = False
def interact_with_reserveplayer(self, reserve, _fleets):
if not self.reserve:
self.reserve = reserve
elif reserve.reserve_number > self.reserve.reserve_number:
self.reserve = reserve
def end_interactions(self, fleets):
if self.player_missing:
if self.reserve:
fleets.remove(self)
capsule = TimeCapsule(2, InvaderPlayer(), self.reserve)
fleets.append(capsule)
fleets.append(TimeCapsule(2.1, PlayerMaker()))
else:
coin.slug(fleets)
Now, we know that, like all the objects in this scheme, the PlayerMaker wants to find out the situation during interactions and then do whatever it does. So in begin_interactions
, it assumes that it has no reserve
, whatever that means, and that the player is missing. If it sees the invaderplayer
, it sets player missing to False: it has seen a player. We anticipate that it will do nothing special in that case. We read on.
If it sees a reserve player, and does not have anything in self.reserve
, it saves that reserve player in self.reserve
. If it does have something in self.reserve
but the number of the incoming reserve is greater than the number of the one it has, it saves that larger one instead.
That was hard to figure out. We owe it to Future Self to do better. What we might do, to make that code more expressive, is to extract a method with an Intention-Revealing Name. Let’s do that:
I try this:
def interact_with_reserveplayer(self, reserve, _fleets):
self.remember_reserve_from_right_hand_end(reserve)
def remember_reserve_from_right_hand_end(self, reserve):
if not self.reserve:
self.reserve = reserve
elif reserve.reserve_number > self.reserve.reserve_number:
self.reserve = reserve
Recall that in game play, there are a few players in reserve, in a row at the bottom. If we’re going to use a reserve player, we want the rightmost one. Hm “rightmost”. Good word. Rename.
def interact_with_reserveplayer(self, reserve, _fleets):
self.remember_rightmost_reserve_player(reserve)
def remember_rightmost_reserve_player(self, reserve):
if not self.reserve:
self.reserve = reserve
elif reserve.reserve_number > self.reserve.reserve_number:
self.reserve = reserve
Better. Now the expression is more clear. We can do better, of course, by noticing the feature envy. The PlayerMaker knows that a ReservePlayer has a reserve-number
and that it increases to the right. It should not know that. It should ask the reserves about their relative positioning.
And here is a chance for a little bit of Programming by Intention: I code this:
def remember_rightmost_reserve_player(self, reserve):
if not self.reserve:
self.reserve = reserve
elif reserve.is_to_the_right_of(self.reserve):
self.reserve = reserve
I have expressed my intention with is_to_the_right_of
: answer whether reserve
is to the right of self.reserve
. There is no such method. PyCharm objects. Objection sustained. We create the method that implements our intention.
class ReservePlayer(InvadersFlyer):
def is_to_the_right_of(self, another_reserve_player):
return self.reserve_number > another_reserve_player.reserve_number
Super. Our interact_with_reserveplayer
expresses its intention better, and we actually programmed a tiny method “by intention”, by typing what we wanted: is_to_the_right_of
, and then implementing it.
Let’s continue with PlayerMaker.
def end_interactions(self, fleets):
if self.player_missing:
if self.reserve:
fleets.remove(self)
capsule = TimeCapsule(2, InvaderPlayer(), self.reserve)
fleets.append(capsule)
fleets.append(TimeCapsule(2.1, PlayerMaker()))
else:
coin.slug(fleets)
What is that code doing? Well, if the player is still missing, that is, if we did not see an interact_with_invaderplayer
event, and if there is a reserve in self.reserve
, we hm what is this all about remove ourself, and append two time capsules in two slightly different ways, one with the reserve player, and one with a new PlayerMaker. If there is no reserve player available, we call coin.slug(fleets), whatever that is.
This isn’t very clear, and if I can figure out a better way to express it, I owe it to Future Me to do it.
Well, we know what PlayerMaker is about, so we actually know just what those four weird lines are doing. They are providing another player so that the human player can continue. And therefore we know what coin.slug
must do: it must be game over.
We express intention, twice. I do the easy one first:
def end_interactions(self, fleets):
if self.player_missing:
if self.reserve:
fleets.remove(self)
capsule = TimeCapsule(2, InvaderPlayer(), self.reserve)
fleets.append(capsule)
fleets.append(TimeCapsule(2.1, PlayerMaker()))
else:
self.game_over(fleets)
def game_over(self, fleets):
coin.slug(fleets)
Game over. Easy. What’s a good phrase for those four lines? How about this?
def end_interactions(self, fleets):
if self.player_missing:
if self.reserve:
self.give_player_another_turn(fleets)
else:
self.game_over(fleets)
def give_player_another_turn(self, fleets):
fleets.remove(self)
capsule = TimeCapsule(2, InvaderPlayer(), self.reserve)
fleets.append(capsule)
fleets.append(TimeCapsule(2.1, PlayerMaker()))
Sure. But now let’s normalize this last method. We should either always have the nested call, or never. Let’s go for never and provide an Explaining Variable Name2.
def give_player_another_turn(self, fleets):
fleets.remove(self)
player_capsule = TimeCapsule(2, InvaderPlayer(), self.reserve)
fleets.append(player_capsule)
maker_capsule = TimeCapsule(2.1, PlayerMaker())
fleets.append(maker_capsule)
OK. What’s the deal with the 2 and 2.1? Good question. Note that the PlayerMaker is removing itself. That’s because once it has decided to give the player another turn, it needs to stop checking for a missing player, since the player will continue to be missing for a while. So it just removes itself from the mix. And that’s why it creates a new PlayerMaker in the second TimeCapsule, to watch out for the demise of the new Player.
We want to be sure that the Maker doesn’t start running before the new Player arrives, so we give it a longer time.
Let’s see if we can express that in this code.
def give_player_another_turn(self, fleets):
fleets.remove(self)
delay_until_new_player = 2.0
player_capsule = TimeCapsule(delay_until_new_player, InvaderPlayer(), self.reserve)
fleets.append(player_capsule)
delay_until_after_new_player_starts = 2.1
maker_capsule = TimeCapsule(delay_until_after_new_player_starts, PlayerMaker())
fleets.append(maker_capsule)
That seems a bit better. What if we reorder the lines, would that make it better?
def give_player_another_turn(self, fleets):
fleets.remove(self)
delay_until_new_player = 2.0
delay_until_after_new_player_starts = 2.1
player_capsule = TimeCapsule(delay_until_new_player, InvaderPlayer(), self.reserve)
maker_capsule = TimeCapsule(delay_until_after_new_player_starts, PlayerMaker())
fleets.append(player_capsule)
fleets.append(maker_capsule)
I think that’s better but it’s rather cluttered, isn’t it? Let’s go back to the other order:
def give_player_another_turn(self, fleets):
fleets.remove(self)
delay_until_new_player = 2.0
player_capsule = TimeCapsule(delay_until_new_player, InvaderPlayer(), self.reserve)
fleets.append(player_capsule)
delay_until_after_new_player_starts = 2.1
maker_capsule = TimeCapsule(delay_until_after_new_player_starts, PlayerMaker())
fleets.append(maker_capsule)
And let’s express the intention of those two three-line chunks:
def give_player_another_turn(self, fleets):
fleets.remove(self)
self.provide_new_player_after_two_seconds(fleets)
self.provide_new_maker_a_little_later(fleets)
def provide_new_player_after_two_seconds(self, fleets):
delay_until_new_player = 2.0
player_capsule = TimeCapsule(delay_until_new_player, InvaderPlayer(), self.reserve)
fleets.append(player_capsule)
def provide_new_maker_a_little_later(self, fleets):
delay_until_after_new_player_starts = 2.1
maker_capsule = TimeCapsule(delay_until_after_new_player_starts, PlayerMaker())
fleets.append(maker_capsule)
I almost like that but now the 2.0 and 2.1 are coupled. Back those changes out:
def give_player_another_turn(self, fleets):
fleets.remove(self)
delay_until_new_player = 2.0
player_capsule = TimeCapsule(delay_until_new_player, InvaderPlayer(), self.reserve)
fleets.append(player_capsule)
delay_until_after_new_player_starts = 2.1
maker_capsule = TimeCapsule(delay_until_after_new_player_starts, PlayerMaker())
fleets.append(maker_capsule)
Reorder the lines and rename:
def give_player_another_turn(self, fleets):
fleets.remove(self)
delay_until_new_player = 2.0
delay_a_bit_longer = 2.1
player_capsule = TimeCapsule(delay_until_new_player, InvaderPlayer(), self.reserve)
fleets.append(player_capsule)
maker_capsule = TimeCapsule(delay_a_bit_longer, PlayerMaker())
fleets.append(maker_capsule)
Now extract methods again:
def give_player_another_turn(self, fleets):
fleets.remove(self)
delay_until_new_player = 2.0
delay_a_bit_longer = 2.1
self.provide_new_player(delay_until_new_player, fleets)
self.provide_new_maker(delay_a_bit_longer, fleets)
def provide_new_player(self, delay_until_new_player, fleets):
player_capsule = TimeCapsule(delay_until_new_player, InvaderPlayer(), self.reserve)
fleets.append(player_capsule)
def provide_new_maker(self, delay_a_bit_longer, fleets):
maker_capsule = TimeCapsule(delay_a_bit_longer, PlayerMaker())
fleets.append(maker_capsule)
We could rename delay_a_bit_longer
to delay_a_bit_longer_to_be_sure_player_is_present_before_maker_starts
, but it would make the code look as weird as the text does here. But even if we don’t do that, we can be darn sure that the 2.0 and 2.1 is intended by whoever wrote this code, so we should be quite aware that we probably shouldn’t just go around changing that.
OK, I think that’s all the squeezing I can do. Let’s reflect and then get to another notion that I want to add into the mix.
Reflection
First of all, I want to notice that it took three or four tries to get something I liked in that little method, and to notice that I actually tried them. I didn’t just think about them: I typed them in and looked at them in place. That’s just fine. I want to engage my sight in deciding about the code. I noticed a cluttered effect at one point, that induced me to change things. If I could listen to the code and sniff it, I might do that too. Fortunately, I cannot sniff my code: some of it would probably put me right off my food.
We’ve focused here on expressing our intention, what the code is trying to accomplish, rather than how it accomplishes it. Perhaps the quintessential example of that was the method is_to_the_right_of
, which just resolves into a comparison of two numbers, but which tells us why we’re doing that comparison.
In that case, we expressed the intention first (although, yes, we did just replace the implementation with the expression) and only then implemented the intention. In a full-on Programming by Intention sequence, we might write two or more calls before refining any of them.
However. If we were to do multiple steps of that, writing some method as a series of calls to two or more other methods that do not presently exist, we’re clearly defining multiple steps that the code does, first this first thing, and then this second thing and finally this third, and before that code works, we will have to finish those three methods.
Working in the Programming by Intention style tends to cause us to take bigger steps, with longer delays between green and green.
To be completely clear, I would prefer the smaller steps almost always. But there are costs to working that way. One is that we may not get a chance to think about the bigger picture. Another is that even if we do think about the bigger picture, we are less likely to have the bigger picture expressed in the code.
Arguably that’s what happened in today’s example. The final method probably grew bit by bit and wound up like this:
def end_interactions(self, fleets):
if self.player_missing:
if self.reserve:
fleets.remove(self)
capsule = TimeCapsule(2, InvaderPlayer(), self.reserve)
fleets.append(capsule)
fleets.append(TimeCapsule(2.1, PlayerMaker()))
else:
coin.slug(fleets)
Perfectly good code, and perhaps there were even tests for it, but the code says what it does, not what it means. Later, much later, i.e. today, we refactored it to express more of its intention:
def end_interactions(self, fleets):
if self.player_missing:
if self.reserve:
self.give_player_another_turn(fleets)
else:
self.game_over(fleets)
And, let’s be honest, there’s still more thinking to be done here than might be ideal. Shall we push this all the way to insanity? Let’s not, but we could extract a method that just says deal_with_missing_player
, and inside that, a method that says another_turn_or_game_over
, and so on, finally winding down to actually doing the work.
You have to draw the line somewhere, and method calls are not free. (However, here they are nearly free, because they only happen once every time the player is killed and they happen right before a forced two second delay, so no matter how many microseconds they require, no one cares.)
But redirecting the mind to a new method isn’t free either, and if we ever do have to figure out how this thing works, there is a cost to the tiny methods that trades off against the improved meaning of each one. And that, I strongly believe, is an individual programmer preference. I like tiny methods. I know people who can’t deal with them. Who’s right? Potayto potahto.
But the point here is expressing intention. We want our code to express all our intentions clearly enough to enable the reader to understand what the is trying to accomplish, what it actually means.
When should we program by intention, and when should we go in the tiniest possible steps?
Frankly, I can’t say specifically when to do one and when to do the other: it’s a judgment call. I have been trying to default to very small steps, because that keeps the program working more of the time, while programming by intention tends to make for a longer series of steps, all of which have to be complete before I can take a breath. I think I reserve programming by intention for things that are clearly algorithmic, with a sequence of actions, perhaps conditional, and I want to see the big picture.
In that other-world problem, that was exactly what I needed. And in that other world, I don’t have very powerful tools, so tiny steps with lots of tests isn’t in the cards. I think, and this is just a guess, that in this Python world right now, I would do well to spike by intention and then roll back and proceed in well-informed small steps.
But we do know that I have a poor record of throwing away my spikes. So, the answer is: I don’t have a good answer about programming by intention. I should emphasize, though, that I want my code always to express intention as clearly as possible and that I want to refactor as needed to get there. And I want to state freely that today’s example shows that I don’t always get there right away. I am imperfect. But I can improve, and sometimes I even do.
Domain-Specific Languages
Now I want to take a bit of a leap.
One of the most powerful techniques we can apply in programming is the creation of a DSL, a Domain-Specific Language, that supports whatever we’re trying to do. Perhaps the most ubiquitous DSL we encounter is HTML, a language that is (supposed to be) good for creating web pages and is (definitely) not good for calculating orbital mechanics. Another well-known DSL is SQL, the language we often use for dealing with our database issues. Gherkin and Gradle are DSLs, and while I could live with Gherkin if I had to, I would walk a long way to avoid Gradle. Some DSLs are more helpful than others.
Frameworks
We do not have a DSL for creating games like Asteroids and Invaders. Arguably we do have a “framework”, a collection of functions and objects that are useful for building such games. In particular, our framework offers — and demands — the interaction event-driven style that we used in today’s example. A different framework might offer a different approach.
A framework quite often takes the form of a library of functions and objects that we can call and instantiate. If we use a conventional programming language like Python to create our game, then, yes, what we have is probably best called a framework. But suppose that instead of using Python or C to build our game, we used UnrealScript, which used to be how you could build games, or parts of games, using the Unreal Engine.
Then we would probably call UnrealScript a DSL.
Programming by Intention vs DSL
How does Programming by Intention relate to Domain Specific Languages? Well, we were certainly writing Python statements today, not RonScript statements, so it’s not really a DSL even if we want it to be.
But we do wind up with statements that begin to read like statements in some other language that is more about games and less about Python or the details of how the game is built:
-
is_to_the_right_of
is a great example. We could implement that for any two objects and then we could say it anywhere, just as if it were a statement in some higher-level language. -
give_player_another_turn
. That’s certainly an idea that might be part of a game DSL. -
TimeCapsule(delay, object_to_add, object_to_remove)
is almost an idea for a DSL component. It’s reaching toward some notion about doing something at some future time.
Let’s Not Go Too Far.
All of these ideas, Programming by Intention, Intention-Revealing Name, Explaining Variable Name, DSL, framework, are al ways we can use to try to make programming easier and understanding our program later easier.
I want to say that as we do more and more with Intention, we get closer and closer to having a Domain-Specific Language for our problem domain, because our intentions will generally be expressed in game terms rather than programming terms. But we can’t push that notion too far because it’s still Python.
But remember Kotlin? It has the notion of “builders”, which are used primarily to create complex objects. When you create a builder in Kotlin, you have in essence defined a simple DSL for building whatever it is that you built. Kotlin has some nifty little syntax tricks that let you set things up to look very unlike Kotlin, and a lot like some kind of domain language, but they’re really still Kotlin. The line gets blurred.
I think it’s fair to say that in Kotlin, you can implement a fairly wide range of Domain-Specific Languages that are still embedded in pure Kotlin. Could we do something like that in Python? Perhaps, but it would not be as clean, because, well, because the languages are different and Kotlin is a lot more friendly toward lambda expressions than is Python.
And I think it is fair to say that as we build up our code, we can benefit from making sure that we express ideas in terms of the domain (Asteroids / Space Invaders), not just in terms of the framework (Fleets, Interactions) or the library/framework (Surface, Vector) or the language (Python).
We are trying to express ourselves, both to the computer, and to the future humans, including future versions of ourselves, who are going to read this code and try to change it. We are creating a language, on top of Python, pygame, and our own mechanical ideas like fleets and interactions. That language communicates, at the same time, with humans and the computer. And since humans have feelings, and are strangely difficult to get things across to sometimes, it may be prudent to pay more attention to the human’s needs that to the computer’s. For now, at least, computers do not have feelings.
One Last Thought
We have a framework, built on top of Pygame, that can implement games like Asteroids and Space Invaders. By my best recollection, we have made no changes to the core framework to get Invaders to run once Asteroids ran. If we changed anything, it wasn’t much. So … if the framework is pretty solid … could we implement a real Domain-Specific Language that could be used to build games like Invaders and Asteroids?
And if we could, what would it look like?
I’m just going to let that idea cook for a while. But it is … interesting.
See you next time!