I'm using a NamedTuple to represent the state of my game while it's running. The user can alter these settings using key presses, so in my PyGame key handler, I'm writing things like:

if key_code == 273:  # Up arrow
    return game_settings._replace(fps=game_settings.fps + 5)

Each key press transforms the settings state by using the old value and _replace.

I decided to try and emulate Clojure's update function as an exercise, and because it may actually make more code more readable. It takes a function and a key, and updates the value at that key using the function. Example usage:

>>> sett = GameSettings(fps=30, is_running=True)

>>> sett.update(lambda x: not x, "is_running")
GameSettings(fps=30, is_running=False)

>>> sett.update2(lambda x: not x, is_running=None)
GameSettings(fps=30, is_running=False)

I created two different versions that each behave similarly but are slightly different. Version one accepts a string key. Version two accepts kwargs with a dummy value. I'm not super happy with either though. Having to pass a String to Version one fells messy any error prone. If I typo the String, I'll get an AttributeError at runtime. Version two accepting a dummy value also feels off and is also error prone; although I do think it reads nicer without the quotes. The code for each is also quite ugly and dense.

The methods don't actually need to accept more than one key (I'll probably never pass more than one at a time), but I figured it barely changes the algorithm and it mirrors _replace, so I decided why not.

I'd appreciate any thoughts here on how to set this up better or make the code cleaner.

from __future__ import annotations
from typing import NamedTuple, Callable, TypeVar
from functools import reduce

T = TypeVar("T")

class GameSettings(NamedTuple):
    fps: int
    is_running: bool = True

    def update(self, f: Callable[[T], T], *keys) -> GameSettings:
        return reduce(lambda acc, k: acc._replace(**{k: f(getattr(acc, k))}), keys, self)

    def update2(self, f: Callable[[T], T], **kwargs) -> GameSettings:
        return self._replace(**{k: f(getattr(self, k)) for k, _ in kwargs.items()})
  • \$\begingroup\$ I'm skeptical that any of this (using an immutable NamedTuple and a lambda) is a good idea at all. I wish you had shown the rest of the key handler function. \$\endgroup\$ Oct 15 '19 at 5:26
  • \$\begingroup\$ @200_success I used an immutable tuple here just because I like representing things as transformations of an immutable state. I come from a mostly FP background, so that's the thought process that I'm used to. Ya, I could have used a dataclass here and it would have been much simpler. The rest of the key handler is just dispatching onto the keys (more of what I posted at the top). \$\endgroup\$ Oct 15 '19 at 11:10

I decided that since I'll likely never need to associate the same function with multiple keys, I might as well just associate the keys with a function directly:

def update3(self, **kwargs: Callable[[T], T]) -> GameSettings:
    return self._replace(**{k: f(getattr(self, k)) for k, f in kwargs.items()})

>>> sett = GameSettings(fps=30, is_running=True)

>>> sett.update3(is_running=lambda x: not x)
GameSettings(fps=30, is_running=False)

The code still isn't beautiful, but it makes a lot more sense when calling now.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.