Joy
Why do I do this? Joy, pure and simple. The sheer pleasure of doing something well, or at least better. Long article, but should be mostly scannable. We begin and end wishing you joy.
I have been privileged1 to spend my life working with software and the people who work with software. I got into it in my youth over six decades ago, and have been there ever since. I’ve done it well, and poorly. I’ve done it successfully and in abject failure. But overall, software development has been a continual source of joy to me.
Those of us who can do anything at all fairly well, and I hope that’s all of us, surely know the pleasure of doing that thing well, of doing it just a bit better than before. It’s not the joy of beating the other guy, it’s just the sheer pleasure of doing the thing well and doing it better. I’ve found that joy, from time to time, in a number of places. The joy in T’ai Chi of finally making that weird backward step while remaining in balance. The joy of a tennis, handball, ping pong exchange where you hit the ball just right. The joy of solving that cast iron puzzle that is so tricky. It comes in many forms.
For me, as a software-oriented person, it comes most often when I’m working with software and make the program just a bit better than it was before. It comes when I learn some new technique, like the unpacking of a dictionary into the parameters of a method. It even comes when someone noticed one of these articles and responds to it.
I do this for that joy.
In particular2 I enjoy taking some code that is OK (or even not so OK) and making it better. More clear, or perhaps just better looking. And that’s what’s going on right now, in our “Strangler” changes to the execute_action
method.
In its old form, still running as the backup method that we’re strangling, it looks like this:
def execute_action_old(self, action_dictionary):
match action_dictionary:
case {'verb': 'add_bot',
'x': x, 'y': y, 'direction': direction}:
self.add_bot_action(x, y, direction)
case {'verb': 'drop',
'entity_object': entity_object,
'holding': holding}:
self.drop_forward_action(entity_object, holding)
case {'verb': 'step',
'entity_object': entity_object}:
self.step_action(entity_object)
case {'verb': 'take',
'entity_object': entity_object}:
self.take_forward_action(entity_object)
case {'verb': 'turn',
'entity_object': entity_object,
'direction': 'NORTH' | 'EAST' | 'SOUTH' | 'WEST' as direction}:
self.turn_action(entity_object, direction)
case {'verb': 'turn',
'direction': bad_direction}:
self._add_message(f'unknown direction {bad_direction}, should be NORTH, EAST, SOUTH, or WEST')
case {'verb': 'NORTH' | 'EAST' | 'SOUTH' | 'WEST' as direction,
'entity_object': entity_object}:
self.turn_action(entity_object, direction)
case { 'verb': verb } if verb not in self.all_verbs:
self._add_message(f'unknown verb: {verb}')
case { 'verb': verb} if verb not in ['add_bot',]:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
case _:
self._add_message(f'Unknown action {action_dictionary}')
And our new form, still incomplete, looks like this:
def execute_action_new(self, verb=None, entity_object=None, **action_dictionary):
if entity_object:
result = True
match verb:
case 'step': self.step_action(entity_object)
case 'drop': self.action_drop(entity_object, **action_dictionary)
case 'turn': result = self.action_turn(entity_object, **action_dictionary)
case _: result = False
return result
else:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
return False
We’ll look at details below, but I hope it’s clear why I prefer the second form to the first. I hope you can even see why I feel a bit of joy now, just in creating it so far, and why, when it’s done, I’m going to feel even more.
There is another kind of joy involved here, and it is this: every time I find a way to make the code better, I get a little jolt of joy right then … but I also build up learning and habits that will increase my joy in the future. I’ll know better ways to do things. I’ll have more techniques at my fingertips. I have joy now, and I set myself up for joy later.
What about you? I write these articles for myself, probably, but I do publish them, and people do read them. You read them, at least sometimes, since you’re here now. I write these things in the hope that they’ll give you ideas that can bring you joy in your software development, or elsewhere in your life3. I’m not here to tell you what you “should” do. I am here, however, to show you how I find joy in the work I’ve done all my life.
Let’s Make Some Joy
OK, let’s get down to it. There’s plenty to do before we’re done. I don’t expect that we’ll finish this cycle today, but we’ll get further along.
Before we pick another verb to do, I want to review what we have so far. It will help orient us, and we may see adjustments that need to be made. And, possibly, we’ll explore a Python feature with some tests.
Let’s quickly scan a bit more of the code that we’re running, starting with execute_action_new
:
def execute_action_new(self, verb=None, entity_object=None, **action_dictionary):
if entity_object:
result = True
match verb:
case 'step': self.step_action(entity_object)
case 'drop': self.action_drop(entity_object, **action_dictionary)
case 'turn': result = self.action_turn(entity_object, **action_dictionary)
case _: result = False
return result
else:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
return False
def action_turn(self, entity_object, direction=None, **action_dictionary):
match direction:
case 'NORTH' | 'EAST' | 'SOUTH' | 'WEST':
self.turn_action(entity_object, direction)
return True
case _:
self._add_message(f'unknown direction {direction}, should be NORTH, EAST, SOUTH, or WEST')
return False
def action_drop(self, entity_object, holding=None, **action_dictionary):
self.drop_forward_action(entity_object, holding)
def drop_forward_action(self, bot, holding):
self.drop_forward(bot, self.entity_from_id(holding))
def drop_forward(self, bot, held_entity):
if self.map.place_at(held_entity, bot.forward_location()):
bot.remove(held_entity)
else:
self._add_bot_message(bot, 'drop location was not open')
Those last two methods are pre-existing. The rest is new. I do think we’ll want to clean up the names, and perhaps arrange the detailed methods differently, but for now I don’t see a pattern or a need for change.
I notice that final return False
in action_turn
. I was thinking that perhaps we wouldn’t be handling everything about turn
and should possibly pass it on to the old method. I think that is mistaken. I believe we’ve handled it just fine. Let’s see about removing that aspect.
The tests pass without it. The quick scan paid off: we noticed a spot that needed a bit of sanding.
def execute_action_new(self, verb=None, entity_object=None, **action_dictionary):
if entity_object:
result = True
match verb:
case 'step': self.step_action(entity_object)
case 'drop': self.action_drop(entity_object, **action_dictionary)
case 'turn': self.action_turn(entity_object, **action_dictionary)
case _: result = False
return result
else:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
return False
def action_turn(self, entity_object, direction=None, **action_dictionary):
match direction:
case 'NORTH' | 'EAST' | 'SOUTH' | 'WEST':
self.turn_action(entity_object, direction)
case _:
self._add_message(f'unknown direction {direction}, should be NORTH, EAST, SOUTH, or WEST')
I’ll double check with the game. Just dandy. Commit: remove possible False return from turn.
I think that ‘take’ should be easy, let’s do that one and then we should look at ‘add_bot’, which I’ve been fearing. I have come to suspect there is nothing to fear.
def execute_action_new(self, verb=None, entity_object=None, **action_dictionary):
if entity_object:
result = True
match verb:
case 'step': self.step_action(entity_object)
case 'drop': self.action_drop(entity_object, **action_dictionary)
case 'take': self.take_forward_action(entity_object)
case 'turn': self.action_turn(entity_object, **action_dictionary)
case _: result = False
return result
else:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
return False
Green. I think it is time to start removing the cases from the old method. I’m sure they’re not being called but part of the fun of strangler is removing the code we’ve replaced.
Removed:
case {'verb': 'drop',
'entity_object': entity_object,
'holding': holding}:
self.drop_forward_action(entity_object, holding)
case {'verb': 'step',
'entity_object': entity_object}:
self.step_action(entity_object)
case {'verb': 'take',
'entity_object': entity_object}:
self.take_forward_action(entity_object)
case {'verb': 'turn',
'entity_object': entity_object,
'direction': 'NORTH' | 'EAST' | 'SOUTH' | 'WEST' as direction}:
self.turn_action(entity_object, direction)
case {'verb': 'turn',
'direction': bad_direction}:
self._add_message(f'unknown direction {bad_direction}, should be NORTH, EAST, SOUTH, or WEST')
Doesn’t that feel good? Yes it does! The simple joy of deleting code! Commit: strangler does ‘take’.
Let’s finish up ‘turn’, the directions as verbs:
def execute_action_new(self, verb=None, entity_object=None, **action_dictionary):
if entity_object:
result = True
match verb:
case 'step': self.step_action(entity_object)
case 'drop': self.action_drop(entity_object, **action_dictionary)
case 'take': self.take_forward_action(entity_object)
case 'turn': self.action_turn(entity_object, **action_dictionary)
case 'NORTH'|'EAST'|'SOUTH'|'WEST' as direction:
self.turn_action(entity_object, direction)
case _: result = False
return result
else:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
return False
Easy-peasy. Green. Commit: direction verbs now in strangler. We’re down to this in the old form:
def execute_action_old(self, action_dictionary):
match action_dictionary:
case {'verb': 'add_bot',
'x': x, 'y': y, 'direction': direction}:
self.add_bot_action(x, y, direction)
case { 'verb': verb } if verb not in self.all_verbs:
self._add_message(f'unknown verb: {verb}')
case { 'verb': verb} if verb not in ['add_bot',]:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
case _:
self._add_message(f'Unknown action {action_dictionary}')
Nothing left but add_bot
and some error handling. We’ll want to replicate that in our new version. But first ‘add_bot’. That one is interesting. The “spec” says that you should provide an entity
(id) of zero. I think that rule is just there because somewhere on the client side we always create an entity ID. Truth be told, I’m not sure. I am sure, however, that we don’t need it, because we create a new ID for the new bot.
We do expect ‘x’, ‘y’, and ‘direction’ as keys in the action dictionary.
Reading the code, I think that if we were to provide a bad direction, we would get an exception. Let’s write a test.
def test_bad_direction(self):
world = World(10, 10)
requests = [ {'verb': 'add_bot',
'entity': 0,
'x': 5, 'y': 5,
'direction': 'WRONG'}]
messages = world.execute_requests(requests)['messages']
print(messages)
assert False
I am somewhat confused because the message is:
[{'message': "verb add_bot requires entity parameter {'x': 5, 'y': 5, 'direction': 'WRONG'}"}]
Ah, the message comes from our new code:
def execute_action_new(self, verb=None, entity_object=None, **action_dictionary):
if entity_object:
result = True
match verb:
case 'step': self.step_action(entity_object)
case 'drop': self.action_drop(entity_object, **action_dictionary)
case 'take': self.take_forward_action(entity_object)
case 'turn': self.action_turn(entity_object, **action_dictionary)
case 'NORTH' | 'EAST' | 'SOUTH' | 'WEST' as direction:
self.turn_action(entity_object, direction)
case _: result = False
return result
else:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
return False
Let’s hack this slightly:
def execute_action_new(self, verb=None, entity_object=None, **action_dictionary):
if entity_object:
result = True
match verb:
case 'step': self.step_action(entity_object)
case 'drop': self.action_drop(entity_object, **action_dictionary)
case 'take': self.take_forward_action(entity_object)
case 'turn': self.action_turn(entity_object, **action_dictionary)
case 'NORTH' | 'EAST' | 'SOUTH' | 'WEST' as direction:
self.turn_action(entity_object, direction)
case _: result = False
return result
else:
if verb != 'add_bot':
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
return False
Now there is no message printed, nor is there an exception. What has happened? The Bot is created correctly, facing EAST somewhere we picked up a default. Ah!
class Direction:
@classmethod
def from_name(cls, string):
match string:
case 'NORTH':
return cls.NORTH
case 'EAST':
return cls.EAST
case 'SOUTH':
return cls.SOUTH
case 'WEST':
return cls.WEST
case _:
return cls.EAST
The class method is extra helpful, defaulting to EAST. Shall we assume that’s the case? Or check? I think we should check before we’re done, even if we do default to EAST. Anyway the test is almost in place, let’s improve it to assume the check.
def test_add_bot_bad_direction(self):
world = World(10, 10)
requests = [ {'verb': 'add_bot',
'entity': 0,
'x': 5, 'y': 5,
'direction': 'WRONG'}]
messages = world.execute_requests(requests)['messages']
expected = 'unknown direction WRONG, should be NORTH, EAST, SOUTH, or WEST'
assert expected in messages[0]['message']
That’s failing as expected. I’m going to treat missing direction as an error and not provide a default.
After a bit more thrashing than I’d have preferred:
def execute_action_new(self, verb=None, entity_object=None, **action_dictionary):
if entity_object or verb == 'add_bot':
result = True
print(f'action new {verb}')
match verb:
case 'add_bot': self.action_add_bot(**action_dictionary)
case 'step': self.step_action(entity_object)
case 'drop': self.action_drop(entity_object, **action_dictionary)
case 'take': self.take_forward_action(entity_object)
case 'turn': self.action_turn(entity_object, **action_dictionary)
case 'NORTH' | 'EAST' | 'SOUTH' | 'WEST' as direction:
self.turn_action(entity_object, direction)
case _: result = False
return result
else:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
return False
def action_add_bot(self, x=None, y=None, direction=None):
print(f'action add bot {x}, {y}, {direction}')
if self.check_add_parameters(x, y, direction):
self.add_bot_action(x, y, direction)
def check_add_parameters(self, x, y, direction):
if not x or not y:
self._add_message('add_bot command requires x and y parameters')
return False
if direction not in ['NORTH' , 'EAST' , 'SOUTH' , 'WEST']:
self._add_message(f'unknown direction {direction}, should be NORTH, EAST, SOUTH, or WEST')
return False
return True
We are green. Commit: add_bot now handled in strangler.
I think we should change that final message to mention ‘add_bot’, because it does not include the full action dictionary as do some of the other messages.
def check_add_parameters(self, x, y, direction):
if not x or not y:
self._add_message('add_bot command requires x and y parameters')
return False
if direction not in ['NORTH' , 'EAST' , 'SOUTH' , 'WEST']:
self._add_message(f'add_bot has unknown direction {direction}, should be NORTH, EAST, SOUTH, or WEST')
return False
return True
I’m glad I did that. Well, sort of glad, but not joyous glad. The test didn’t break, which tells me that the old message is still running, which I think says we have gotten into execute_action_old
.
Oh my mistake. The test finds a subset of the string. Change it:
def test_add_bot_bad_direction(self):
world = World(10, 10)
requests = [ {'verb': 'add_bot',
'entity': 0,
'x': 5, 'y': 5,
'direction': 'WRONG'}]
messages = world.execute_requests(requests)['messages']
expected = 'add_bot has unknown direction WRONG, should be NORTH, EAST, SOUTH, or WEST'
assert messages[0]['message'] == expected
Commit: improve test.
Let’s see what’s left in the old method:
def execute_action_old(self, action_dictionary):
match action_dictionary:
case { 'verb': verb } if verb not in self.all_verbs:
self._add_message(f'unknown verb: {verb}')
case { 'verb': verb} if verb not in ['add_bot',]:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
case _:
self._add_message(f'Unknown action {action_dictionary}')
I think we’re about ready to get rid of this. Let’s see if it’s being used. I put an assert False at the top. Four tests fail.
def test_no_verb(self):
world = World(10, 10)
requests = [{'entity_object': "fake", 'vorb': 'add_bot'}]
messages = world.execute_requests(requests)['messages']
assert 'Unknown action' in messages[0]['message']
This one should be easy:
def execute_action_new(self, verb=None, entity_object=None, **action_dictionary):
if entity_object or verb == 'add_bot':
result = True
match verb:
case 'add_bot': self.action_add_bot(**action_dictionary)
case 'step': self.step_action(entity_object)
case 'drop': self.action_drop(entity_object, **action_dictionary)
case 'take': self.take_forward_action(entity_object)
case 'turn': self.action_turn(entity_object, **action_dictionary)
case 'NORTH' | 'EAST' | 'SOUTH' | 'WEST' as direction:
self.turn_action(entity_object, direction)
case _:
self._add_message(f'Unknown action {verb=} {action_dictionary=}')
return result
else:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
return False
That passes. Now:
def test_bad_verb(self):
world = World(10, 10)
requests = [{'entity_object': "fake", 'verb': 'whatever'}]
messages = world.execute_requests(requests)['messages']
assert 'unknown verb: whatever' in messages[0]['message']
Ah. This is really going to be the same error. Let me change the test.
def test_bad_verb(self):
world = World(10, 10)
requests = [{'entity_object': "fake", 'verb': 'whatever'}]
messages = world.execute_requests(requests)['messages']
assert "Unknown action verb='whatever'" in messages[0]['message']
Next:
def test_no_entity(self):
world = World(10, 10)
requests = [ {'verb': 'step'}]
messages = world.execute_requests(requests)['messages']
assert 'requires entity parameter' in messages[0]['message']
Failure is:
def execute_action(self, action_dictionary):
if not self.execute_action_new(**action_dictionary):
> assert False
E AssertionError
We returned false. I think we have to stop doing that. I continue to call the old method but remove all its guts.
def execute_action_old(self, action_dictionary):
print(f'old called with {action_dictionary}')
Everything passes! I did not expect that. Adding an assert False, I find two tests failing, one is the no entity one:
def test_no_entity(self):
world = World(10, 10)
requests = [ {'verb': 'step'}]
messages = world.execute_requests(requests)['messages']
assert 'requires entity parameter' in messages[0]['message']
This ran when execute_action_old
was empty. That tells me that we filed a suitable message. What’s the other?
def test_zero_id(self):
WorldEntity.next_id = 100
world = World(10, 10)
command = {'entity': 0,
'verb': 'step'}
rq = [ command ]
messages = world.execute_requests(rq)['messages']
assert 'requires entity parameter' in messages[0]['message']
So if these are passing with the old method doing nothing, we should be able to remove it and not call it.
Yes! This passes:
def execute_action(self, action_dictionary):
self.execute_action_new(**action_dictionary)
def execute_action_new(self, verb=None, entity_object=None, **action_dictionary):
if entity_object or verb == 'add_bot':
match verb:
case 'add_bot': self.action_add_bot(**action_dictionary)
case 'step': self.step_action(entity_object)
case 'drop': self.action_drop(entity_object, **action_dictionary)
case 'take': self.take_forward_action(entity_object)
case 'turn': self.action_turn(entity_object, **action_dictionary)
case 'NORTH' | 'EAST' | 'SOUTH' | 'WEST' as direction:
self.turn_action(entity_object, direction)
case _:
self._add_message(f'Unknown action {verb=} {action_dictionary=}')
else:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
Remove the stub method and rename the good one. We need to change the call as well:
def execute_actions(self, actions_list):
for action in actions_list:
if isinstance(action, dict):
action_with_parameters = self.assign_parameters(**action)
self.execute_action(**action_with_parameters)
else:
self._add_message(f'action must be dictionary {action}')
def execute_action(self, verb=None, entity_object=None, **action_dictionary):
if entity_object or verb == 'add_bot':
match verb:
case 'add_bot': self.action_add_bot(**action_dictionary)
case 'step': self.step_action(entity_object)
case 'drop': self.action_drop(entity_object, **action_dictionary)
case 'take': self.take_forward_action(entity_object)
case 'turn': self.action_turn(entity_object, **action_dictionary)
case 'NORTH' | 'EAST' | 'SOUTH' | 'WEST' as direction:
self.turn_action(entity_object, direction)
case _:
self._add_message(f'Unknown action {verb=} {action_dictionary=}')
else:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
Now we can delete the old method entirely. Our property all_verbs
is no longer used. Remove that. Green. Commit: tidying.
We’re at a decent stopping point. Let’s reflect and sum up.
Summary
The good news is that we have put the simple execute_action
above in place of this one:
def execute_action_old(self, action_dictionary):
match action_dictionary:
case {'verb': 'add_bot',
'x': x, 'y': y, 'direction': direction}:
self.add_bot_action(x, y, direction)
case {'verb': 'drop',
'entity_object': entity_object,
'holding': holding}:
self.drop_forward_action(entity_object, holding)
case {'verb': 'step',
'entity_object': entity_object}:
self.step_action(entity_object)
case {'verb': 'take',
'entity_object': entity_object}:
self.take_forward_action(entity_object)
case {'verb': 'turn',
'entity_object': entity_object,
'direction': 'NORTH' | 'EAST' | 'SOUTH' | 'WEST' as direction}:
self.turn_action(entity_object, direction)
case {'verb': 'turn',
'direction': bad_direction}:
self._add_message(f'unknown direction {bad_direction}, should be NORTH, EAST, SOUTH, or WEST')
case {'verb': 'NORTH' | 'EAST' | 'SOUTH' | 'WEST' as direction,
'entity_object': entity_object}:
self.turn_action(entity_object, direction)
case { 'verb': verb } if verb not in self.all_verbs:
self._add_message(f'unknown verb: {verb}')
case { 'verb': verb} if verb not in ['add_bot',]:
self._add_message(f'verb {verb} requires entity parameter {action_dictionary}')
case _:
self._add_message(f'Unknown action {action_dictionary}')
I hope the value of that is pretty obvious. The new version is 15 lines instead of 38, and consists of simple matches on verb rather than complicated dictionary matches. Each of its active elements is a single line instead of two, three, or four. (OK, OK, right, one of them is still two lines.)
We have equivalent, though slightly different error messages. We have a slight difference in spec, I think: I believe that we can now add a bot without saying 'entity': 0
. Maybe I’ll write a test for that.
There is still come cleanup to be done, but I am tired and it is time for a break. The cleanup will focus on the methods we call from execute_action
, giving them more consistent names and ensuring that they divide up responsibility appropriately. Sometimes we need to parse out additional parameters, and sometimes we do not. Then some of them take two quick steps to get the job done, and some just do one more.
And names like action_turn
and turn_action
just won’t do. We’ll address this cleanup next time, most likely, unless I get a better idea.
But the topic here is joy. I truly enjoy doing what we just did, and I really enjoy seeing how much better execute_action
is than it was only a day ago.
I wish you joy, not just the “joy of the season”, but in all your enterprises, all your life. See you next time!
-
I mean “privilege” informally, but also in the more modern sense of getting a better deal than some others in our society. See Privilege—I’ve Got it for thoughts on that. ↩
-
REDACTED these six fish in particular. NSFW, you have been warned. ↩
-
One of my friends has taken up knife-making. He’s beginning to find the joy of the craft, the art, of making something with his hands. He’s pleased when someone else likes what he’s made, but the real delight to me is to see that he’s beginning to find the inherent joy of making the thing. I wish everyone could find that joy, whether it’s in software, knife-making, knitting, whatever. ↩