What's Next?
Hello, loves!
The new scheme for Content seems to be working out well. Remaining are two concerns …
Well, I say two, though there are surely many things to be concerned about. The two I have in mind are:
- The upcoming Content items require “instance variables”, such as the cycling flag and time counter for the Animated item, that are not included in the standard batch. We’d like to make them optional.
- The upcoming items need to be subscribed to some events. We need a way to represent those without adding code to the Content class.
I have at least two options before me for the additional variables: In Python we can always just store new items as if they were already there, albeit with a bit of complaining from PyCharm. Or we could pass in a little package, say info, of named variables and access it indirectly self.info.cycling.
The second of these would leave the Content item nearly immutable, and if we moved state inside one of these info things, it could be entirely immutable. I’m not sure why I would care about that, however, since in principle, a Content item is stateful and we mean it to be.
Another nail in the coffin of just slamming them in: our values need initialization and we’d have to provide a place for the init code to reside.
As for subscriptions, I see at least two options as well. We could pass the Content constructor a list of events to subscribe to, including event, caller id, and the callback function. Alternatively, we could provide an additional callback initialize, containing subscription code, which would be called by the item after its set up.
A third option comes to mind. Would it be possible to subscribe during the execution of the class method? I think not … a callback often needs to publish a message, and some of those require the self parameter to be correctly set. I think we’ll not explore this idea further at this time.
I think we’ll start with the notion of an additional info package that is provided to the CombinedContent constructor, containing whatever additional variables a particular content type may need. And we’ll try providing a table of subscriptions to the constructor, to be subscribed on our behalf.
We’ll see what all this looks like, and if we like it, go with it, and if we don’t, we’ll change it. We are in the business of changing code.
Let’s have a test for the info package.
def test_info_package(self):
info = SimpleNamespace(cycling=True, count=0)
content = CombinedContent(name='item',
resources = [],
scale = 1,
info=info
)
assert content.info.cycling
content.info.count += 1
assert content.info.count == 1
We don’t currently support info in the class, so the test is not happy yet.
class CombinedContent:
def __init__(self, *, name, resources, scale,
interaction=lambda self, interactor: True,
info=None):
self.name = name
self.resources = resources
self.scale = scale
self.state = 0
self.info = info
self.interact_with_player = types.MethodType(interaction, self)
I think defaulting to None will be fine, as no one had better be accessing info without providing it and if they do we have a chance of finding out about it soon.
Now both our remaining Content subclasses, AppearingContent and Animated, require subscriptions. Let’s see if we can make that work. Again with a test.
I’ve gotten quite far before hitting a snag. Here’s the test:
def test_subscriptions(self):
count = 0
def increment():
nonlocal count
count += 1
sub = SimpleNamespace(event='set_state',
caller='',
callback=increment)
content = CombinedContent(
name='item',
resources = [],
scale=1,
subs=[sub]
)
self.pub_sub.publish('set_state', content=self, state=1)
assert count == 1
I think that should work when we’re done. Here’s where I am in CombinedContent:
def __init__(self, *, name, resources, scale,
interaction=lambda self, interactor: True,
info=None,
subs=[]):
self.name = name
self.resources = resources
self.scale = scale
self.state = 0
self.info = info
self.interact_with_player = types.MethodType(interaction, self)
for sub in subs:
What I want to say inside the loop is something.publish(sub.event, sub.caller, sub.callback). The snag is that the CombinedContact object does not have access to anything that understands publish …
But … but … but but but … doesn’t the Dungeon call run? Ha! yes it does. so we can put the loop in run! Whew! I was afraid we’d have to do something nasty.
Fix that up. After a bit this test runs:
def test_subscriptions(self):
count = 0
def increment(*, content, state):
nonlocal count
print('counting')
count += 1
sub = SimpleNamespace(event='do_it',
caller='xxx',
callback=increment)
content = CombinedContent(
name='item',
resources = [],
scale=1,
subs=[sub]
)
content.run(self)
self.pub_sub.publish('do_it',
caller_id='xxx',
content=self,
state=1)
assert count == 1
I think I have discovered an anomaly. If I set the caller_id to ‘’ in the publish, the callback is called twice. I think there needs to be a rule, that while you can subscribe with a blank caller id, you cannot publish without one. I’ve made a note to fix it.
Here’s the CombinedContent class:
class CombinedContent:
def __init__(self, *, name, resources, scale,
interaction=lambda self, interactor: True,
info=None,
subs=[]):
self.name = name
self.resources = resources
self.scale = scale
self.state = 0
self.info = info
self.subs = subs
self.interact_with_player = types.MethodType(interaction, self)
def run(self, dungeon):
for sub in self.subs:
dungeon.subscribe(sub.event, sub.caller, sub.callback)
There’s also the class method for lever, not shown.
Using a SimpleNamespace for the subscription table won’t do. We need a class for that. I’d like to call that Subscription, I think. It’s specific to Content, though. ContentSubscription seems undesirably long. I think we’ll define the Subscription as part of PubSub’s file and even provide a method to subscribe a collection of them, so that CombinedContent can just pass them on.
We’ll do this by Wishful Thinking, where we write the code that needs to work and then make it work. But first, we need a commit: Adding info package and subscriptions.
Now the code that we want to have work:
def test_subscriptions(self):
count = 0
def increment(*, content, state):
nonlocal count
print('counting')
count += 1
sub = Subscription(event='do_it',
caller_id='',
callback=increment)
content = CombinedContent(
name='item',
resources = [],
scale=1,
subs=[sub]
)
content.run(self)
self.pub_sub.publish('do_it',
caller_id='xxx',
content=self,
state=1)
assert count == 1
We’ll go in two steps, first the new class, then the group subscription.
Over in PubSub:
class Subscription:
def __init__(self, *, event, caller_id, callback):
self.event = event
self.caller_id = caller_id
self.callback = callback
I’m leaning toward keyword arguments more and more. They seem to help reduce mistakes.
I used caller_id instead of caller, as it is the term used in PubSub, and changed the test.
This is green. Commit. Now on behalf of CombinedContent, we wish for this:
def run(self, dungeon):
dungeon.subscribe_all(self.subs)
We need to thread that method through all the pub_sub forwarders so we add it to the protocol and fix up the dungeon and our test to forward.
class PubSubProtocol(ABC):
@abstractmethod
def subscribe(self, event, caller_id, callback):
pass
@abstractmethod
def subscribe_all(self, subscriptions):
pass
@abstractmethod
def subscribe_once(self, event, caller_id, callback):
pass
@abstractmethod
def publish(self, event, caller_id, *args, **kwargs):
pass
class PubSub(PubSubProtocol):
def subscribe_all(self, subscriptions):
for subscription in subscriptions:
self.subscribe(subscription.event, subscription.caller_id, subscription.callback)
Green. Commit. I think it’s time to sum up, I’m getting tired and when I’m tired I start making even more mistakes than when I’m fresh, and that’s way too many mistakes. Also, it’s less fun.
Summary
I think we’re ready now to implement the Appearing and Animated content items, or close enough, if not completely ready. But I think we’ve got everything we need. And it all went well, with a few small step commits, and only one difficulty. The difficulty had to do with matching up the subscription event and caller ID with the expected callback format, since each publication can provide any arguments it wants to, and the receivers had better be ready for them. I’ve been stumbling over this issue for quite a while now, and I would like to have something better: I just don’t know yet what that might be.
All that aside, I think we have about to accomplish what I’ve been striving for, a version of Content that doesn’t require multiple subclasses and masses of code. Our new constructors are quite declarative rather than procedural, and about the same size as before. We no longer need to provide any specialized setup code to be executed. Instead we provide data to the CombinedContent class and it does what needs to be done. And, what needs to be done is currently expressed as just one line of code.
I’m leaning slightly toward creating a ContentFactory object that builds content items. That will keep the Content class itself limited to just the init and run, without class methods to confuse the issue. We might even find some savings with the factory object itself. If nothing else, maybe we can find a way to make the resource lists not quite so prominent and ugly, with those long file paths.
Notes currently include:
- Force a caller_id on publish to avoid double calls;
- Find a way to stop Arcade from swallowing errors;
- Allow for a method as a callback in CombinedContent and don’t wrap it.
There are many more notes: those are just the ones germane to the content refactoring.
I expect that today or tomorrow we’ll change all the existing content creation to use CombinedContent, remove the other classes, and rename CombinedContent to Content. That I think we’ll have approximately the same amount of code, perhaps a bit less, but it will all be setup, factory methods, with the run-time objects all supported by the tiny new Content class.
I remain pleased. I’d been feeling that there must be a way to simplify content, and tried a few different ways that didn’t work out. This one seems quite good.
- Aside (CW: LLM, bail now, Hill!)
-
I wonder about my colleagues who are using LLMs to build real code, not just for asking the occasional question. Do they ever manage to get solutions that are as nice as this one? Or do they stop when things work?
-
And I wonder, when the LLM tries to change that code a month from now, what happens. I suspect it won’t be anything really good for most LLM users.
-
I wonder