Docker in Continuous Integration

Docker – or more general containerization – can be applied in several areas of software development to improve many aspects of our work. The most common are:

  • In development to easily setup an environment for running and developing the software without installing every manually on the target OS.
  • For deployment either in docker-stacks (e.g. managed by portainer) or kubernetes only requiring hosts able to run containers without specialized setups.
  • For building software artifacts on your continuous integration (CI) infrastructure with the same benefits as the former two applications.

In essence you trade snowflaking all the involved machines (dev, CI nodes, servers) for some additional complexity around Dockerfiles, images, containers and their orchestration.

In this post I want to focus on the CI part:

Using Docker in CI

Administrators and infrastructure people usually will cheer if they just need to provide docker-capable nodes for developers to build their projects on. No need to provide certain runtimes, libraries, tools and configurations anymore. No need to negotiate with developers about the environment all the time and keeping it updated, appropriate and secure.

Developers on the other hand need to work with another tool encapsulating their build process. While this gives them a lot of freedom in choosing and shaping the environment for their builds (the “inside” of the containers) it adds pitfalls and complexity on the outside.

If your artifact is not (only) another docker image but things like test- and coverage reports, binaries or other files you have to move stuff from inside the containers to the outside aka host. In CI use cases you have essentially the following alternatives, each with different pros and cons:

Bind mounts for a container run

In this approach you build your environment using a normal docker build command and follow it with a docker run, e.g.:

docker build -t build-project .
docker run --rm -u `id -u` -v `pwd`:/build build-project

Pros:

  • This approach feels natural and does automatic container cleanup by using the --rm option on docker run.
  • It also enables dependency caches etc. on the host which can reduce build times, even across projects.
  • Sometimes you do not even need a dockerfile but can use plain docker images without building one yourself, e.g. eclipse-temurin:21-jdk.

Cons:

  • It actually runs a container and has limited file system access to the host.
  • It may leave build artifacts and temporary files on the host.
  • It may create file ownership issues, hence the -u `id -u` arguments to docker run in the example.

Image build and container to copy from

This approach is similar to above in that is consists of several steps but it does not need mounts and does most of the stuff during docker build:

set -eu
docker build -t build-project .
container_id=$(docker create build-project)
trap 'docker rm -f "$container_id" >/dev/null 2>&1 || true' EXIT
docker start $container_id
docker cp $container_id:/buildresults/ ./artifacts/

Pros:

  • Most stuff happens inside docker build.
  • You can use layer caching to improve build times.
  • Cleanup is done using shell mechanismns (trap), can also be performed using different means.
  • What leaves the container is exactly and explicitly controllable

Cons:

  • You need to run a container
  • You need to take care of container cleanup
  • Full benefit of layer caching may require thought/engineering to improve build times

Multi-stage build with scratch-image output

In this approach we never actually run a container and let docker build copy the specified artifacts to the host using a multi-stage build. Most of the interesting bits are in the Dockerfile while the build call looks like:

docker build --output artifacts/ .

The Dockerfile gets a bit more complex:

FROM python:3.14 AS build
WORKDIR /build

COPY . .

RUN pip3 install --no-cache-dir -r requirements.txt
RUN python3 -m build

FROM scratch AS package
COPY --from=build /build/dist/*.whl .

Pros:

  • No need to explicitly cleanup containers or dependency caches
  • Layer caching possible
  • No need to run a container explicitly, building the image is enough
  • Only one simple call in the build pipeline

Cons:

  • More complexity inside the Dockerfile

Conclusion

Using docker for building software has many advantages with the price of an additional tools and its own complexities. Several, easily adaptable approaches exist to facilitate containerization in CI environments. Use the one that fits your requirements and environment best.

Which approach do you like best? What are you using in your build and delivery pipelines?

I would be glad to hear your thoughts and comments.

Dynamic Device Classes in Python

When writing PyTango device servers, it is common to implement one Python class per Tango device class. For small projects, this approach is simple and easy to understand. However, it becomes cumbersome when the set of available devices is not known at development time.

Consider a device server that should be entirely driven by a configuration file. Instead of hard-coding every supported device class, the server reads the device definitions at startup and creates the required Tango device classes automatically.

The goal is to write a generic PyTango server that does not need to be modified whenever a new device class is added. If a new entry appears in the configuration file, the server should simply create the corresponding Tango device class.

Python type

At first glance, this sounds unusual. After all, Python classes are typically defined using the familiar class keyword:

class Motor(Device):
    pass

Most Python developers stop here and never think about how classes are actually created. Under the hood, however, classes are objects themselves, and Python provides a built-in mechanism to construct them dynamically.

The function responsible for this is type().

Most of us use it in its simplest form to inspect the type of an object:

print(type(42))
# <class 'int'>

Less well known is its three-argument form:

type(name, bases, attributes)

The arguments are:

  • name: the name of the new class.
  • bases: a tuple of parent classes.
  • attributes: a dictionary containing class attributes and methods.

This means that the following definition is equivalent to the above class definition.

Motor = type(
    "Motor",
    (Device,),
    {},
)

The resulting object is exactly the same: a Python class that can be instantiated or registered with PyTango.

The third argument of type() becomes particularly interesting when more than just the class name should be configurable. The attributes dictionary allows methods, properties, or other class members to be added dynamically. This can be useful when Tango attributes or commands are also described in the configuration file.

Motor = type(
    "Motor",
    (Device,),
    {
        "some_property": 42,
    },
)

a = Motor()
print(a.some_property)
# 42

Conclusion

For our use case, creating classes dynamically becomes straightforward. Reading the configuration file and creating the required classes can be done in a simple loop. From PyTango’s perspective, there is no difference between a statically defined class and one created dynamically using type(). Both behave like ordinary Python classes.

AI Code Won’t Be for Humans Much Longer (AI impressions, part 2 of 5)

This is the second part of the series “Impressions of Our Current AI Usage”, as outlined by the introduction article.

In the first years of software development, the word “source code” didn’t exist, because code was just that: encoded machine instructions. How they were encoded changed rapidely, from flipping bits in the RAM directly by mechanical switches over feeding paper tapes with punched holes to magnetic storage. But for a long time, we worked with none or little abstraction over the actual machine code. I remember assembler code listings that had two columns of text: the first column for the computer, the second one just translating the first column into human-readable text.
And even with this little bit of clarification what the code actually does, we already needed additional software that took our source code and translated it for the machine.
With the adoption of higher-level programming languages, the additional software stack grew in depth until the distance between the source code and the actual machine instructions was big enough to warrant an intermediate layer of representation. Programming languages like Java or C# put a “byte-code layer” between our textual source code and the binary machine code. The machine we program against is no longer a real computer, but a “virtual machine” or in better words, a model of a machine. As long as we write source code that works correct with the model, we can assume that all the translation layers will find a way to run it correctly on the real computing substrate.
We are used to this kind of programming. We describe our goals using the machine model and a sophisticated machinery of software and hardware parts make it happen.

Forward to today and we use artificial intelligence (or inference using another kind of “model”) to produce source code in our favorite programming languages by describing our goals in even broader terms than before. We might mistake our prompts for natural language and think that we are able to produce source code by just saying what we want.

The question that poses itself nearly instantly is: If we invented a device that transforms natural language into machine behaviour from scratch today, would we include all the intermediate layers into its inner works? Is it really a good idea to transform natural language into higher-level source code, compile the source code into byte code and do all the weird magic to come up with a sequence of machine instructions that resemble the byte code? Isn’t it more efficient to teach the inference how the actual machine works and let it program directly?
Or, asking from the other side, who is the target audience of the generated source code if nobody reads it and the compiler only parses it once? Why does the inference invent all the variable and method names when the compiler throws them away again in the first step of its processing? Of course, right now the inference only imitates our way of working. But we work like we do because we write source code for other humans. If the human at the helm can’t read any layer of code anyway, why not jump directly to the most obscure representation of code and skip all the readability requirements?

As soon as the inference doesn’t imitate but actually learns about the target computing substrate, it will produce working code that is undecipherable for human readers, but optimal for the machine. (If you want to experience this effect in a tiny dose, I encourage you to play the “TIS-100” programming game). And because most inference users don’t need the readable code anyway, they won’t miss anything and get faster solutions with less hassle.

So my guess is that today’s source code will be a dying art, invented for humans and ignored by the machines because it doesn’t provide anything useful for them. The source code of the future will be less readable, more enigmatic and probably more efficient for the machine. Which means that human intervention or even just participation in the software production process will become more cumbersome and therefore even more limited.

My sorrow is that this distancing of the programming process from actual human oversight might provide a hard depedency on inference work alone. It would mean that humans aren’t just scales slower than the inference, but actually incapable of doing its work by hand anymore.

Quotes Are Not Part of the Argument

Recently, we upgraded a project from an older Java version to a newer one. Many of the changes were routine: update dependencies, replace deprecated APIs, fix a few compiler errors and run the test suite.

One of these changes concerned the invocation of an external Windows program.

The application used the deprecated overload:

Runtime.getRuntime().exec(command);

The command was assembled as one long string. The external program accepted command-line parameters of the following form:

/a="value of A" /b="value of B" /c="value of C"

Because the parameter values could contain spaces, we enclosed them in double quotes. We even had unit tests that explicitly verified the quoting. It was an important detail, or so we thought.

As part of the upgrade, we switched to the recommended overload that accepts the executable and its arguments separately:

Runtime.getRuntime().exec(commandArray);

The migration seemed straightforward. Instead of joining the executable and all parameters into one command string, we put them into a string array:

String[] command = {
"program",
"/a=\"value of A\"",
"/b=\"value of B\"",
"/c=\"value of C\""
};
Runtime.getRuntime().exec(command);

The unit tests were still green. The quotes were still present. Everything looked correct.

Then the application was deployed.

Invalid switch

In production, the external program stopped accepting our invocation. Its only diagnostic message was:

Invalid switch

This was not particularly helpful.

We inspected the parameters. All switches were present. Their spelling was correct. Their order was correct. The values were correct. The quote characters were exactly where our tests expected them to be.

Even more confusingly, the command worked perfectly when entered manually in a Windows command prompt:

program /a="value of A" /b="value of B" /c="value of C"

The executable clearly supported these parameters. The shell command clearly worked. And our Java code appeared to produce the same command.

But it did not.

Asking the receiving program

After spending some time comparing strings and staring at quote characters, we decided to stop reasoning about what the external program ought to receive. Instead, we wrote a small program that showed us what it actually received:

void main(String[] args) {
for (var i = 0; i < args.length; i++) {
System.out.println("args[" + i + "]: '" + args[i] + "'");
}
}

We packaged it as a JAR and invoked it from cmd.exe using the same parameter structure:

java -jar exec-experiment.jar /a="value of A" /b="value of B" /c="value of C"

The output was:

args[0]: '/a=value of A'
args[1]: '/b=value of B'
args[2]: '/c=value of C'

The double quotes were gone.

This was the missing piece.

The quotes in a shell command are not necessarily characters intended for the receiving program. They are instructions to the command-line parser. They tell it that a sequence containing spaces belongs to one argument.

The value

/a="value of A"

does not mean that the program receives an argument containing two quote characters. It means that the program receives one argument rather than three:

/a=value of A

The quote characters control parsing. They are not part of the resulting argument.

Quotes can appear in surprising places

To verify this interpretation, we performed a slightly more unusual experiment:

java -jar exec-experiment.jar /"a="va"lue of A" /b="value of B" /c="value of C"

This command is certainly not how anybody would normally write the parameters. Nevertheless, its output was unchanged:

args[0]: '/a=value of A'
args[1]: '/b=value of B'
args[2]: '/c=value of C'

The quotes can surround different portions of a token. Their purpose is to influence how the command line is divided into arguments. Once parsing is complete, they disappear.

This distinction is easy to overlook because a command line is usually presented as a string. It looks as though this string is passed to the program. In reality, there are two different representations involved:

program /a="value of A"

is a textual command line that still needs to be parsed.

By contrast,

new String[] {
"program",
"/a=value of A"
}

already describes the result of that parsing: an executable followed by one complete argument.

We had moved the parsing boundary

With Runtime.exec(String[]), every array element already represents one argument. Spaces inside an element do not split it into additional arguments.

By retaining the quotes, we had changed their meaning. They were no longer syntax interpreted by a shell-like parser. They had become literal characters inside the argument:

/a="value of A"

That was not what the external program expected. It expected:

/a=value of A

The error message “Invalid switch” was therefore accurate, but not very illuminating. The switch looked correct in our logs because we were looking at its command-line representation rather than at the argument format expected by the program.

The fix was simple:

String[] command = {
"program",
"/a=value of A",
"/b=value of B",
"/c=value of C"
};
Runtime.getRuntime().exec(command);

Or, preferably, using ProcessBuilder:

Process process = new ProcessBuilder(
"program",
"/a=value of A",
"/b=value of B",
"/c=value of C"
).start();

After removing the quote characters, the external program worked again.

Ironically, the quotes that our old implementation and its tests had treated as essential were exactly what broke the new implementation.

The takeaway

A command line and an argument array are not interchangeable representations.

When constructing a command line, quoting may be required to preserve spaces during parsing. When constructing an argument array, parsing has already happened conceptually. Each element is one argument, spaces included.

Do not ask:

How would I type this command in a shell?

Ask:

What exact strings should the receiving program find in its argument array?

The answer to the second question is what belongs in Runtime.exec(String[]) or ProcessBuilder.

Sometimes an API migration changes more than a method signature. It moves a boundary – in this case, the boundary between formatting a command line and supplying already separated arguments.

And when that boundary moves, yesterday’s carefully tested solution can become today’s bug.

Great software engineers are transformers

Created by me; using https://www.deviantart.com/dreamup

…or transformators. Often when I meet people and talk to them about their jobs or everyday life topics come up about suboptimal processes and workflows and other complexeties we have to deal with. Almost everytime such issues arise my brain start a background process working on ways to improve the talked-about situation.

Many of my developer colleagues and friends are the same: We all shake our heads or face palm and immediately think about possible remedies.

Talking to non-developers about the same issues often leads to reactions like

  • It cannot be changed
  • It has been this way since forever
  • We have never done it that way

None of the great (software) engineers I had the pleasure to work with thinks this way:

They all try to understand the context, domain and status quo. As a part of this process often the first problems are uncovered. In addition we define aims and target metrics together with the domain experts who often are part of one of several user groups.

On that basis they develop solutions fitting to the situation at hand. All the constraints in time, resources and knowledge are taken into account. Knowing that the initial scope usually does not cover a full solution, an evolvable system is designed that already provides value. Over time they add features, fix blind spots and weaknesses and gradually expand the scope.

This analytic view and the incremental process towards a system that improves the current situation is key in pushing things forward. It also eliminates most of the “impossible to implement/change” counterargument.

The latter two main arguments against change revolve often about excluding user groups like elderly people or people accustomed to the status quo unwilling to adapt. They can be mitigated by designing the systems and services to have multi-modal inputs and outputs. Working with them can stay largely unchanged for inert users groups while others may utilize the new options the solutions offer.

Recently, I heard of a nice example of this: Customer banking already is largely digitalized but there are banks that still offer inboxes for credit transfer on paper in addition to online-banking and apps. People without digital devices or knowledge simply throw the papers into the inbox where it automatically gets scanned and digitally processed. And there is still the option to snail-mail the account statements for the people who do their management on paper. All the process in between is automated and digital leaving no one behind.

In https://schneide.blog/2026/01/19/digitalization-is-hard-especially-in-germany/ I described general guidelines to make mostly analog real-world processes digital and frictionless.

When Optional sounds too optional: opt for more expressive types

So, I have one PyQt application which not only is quite data-heavy, but also has significant real-time requirements, as well as multiple windows. This construct brings some absolutely horrifying highly intellectually inspiring quests with it, and Python turned out to be kind of a good decision for that project, because that is one of the languages where, when you think about your structure a bit, you might get to write very natural-sounding like code.

Of course, the following idea is actually language-agnostic, I will just use fictive Python examples close to problems-based-on-a-true-story.

This in itself is not only a matter of aesthetics, but because real-time demands are quite tricky to reliably be covered by unit tests alone, the actual code has to read itself so clearly that one does not need to second-guess what any of this does. Think of a bedtime story, which usually would not, coming to think of it, contain clauses – or paragraphs, for that matter – requiring, under circumstances not even trivial to the human eye, one kind of meticulous gymnastics, easily negating twice, or thrice, and relying on Python’s borderline criminal degrees of freedom in duck typing, or canards even– you see — your toddler will now not go to sleep anytime soon. Or trust you with another story, for that matter.

Now I found out: While Qt is somewhat mature, one cannot even trust their way of doing things – i.e. turns out, the signals/slots system is not particularly designed for performance. Neither did I feel inclined to put my faith into even another state management solution like e.g. python-statemachine package, because – as capable as that sounds, it might be overkill, and distracting with its own idiosyncrasies (as also: I would not recommend Redux for a web project anymore, especially in TypeScript, except for you really know from the start that this is a good fit).

But, so, I have some tricky interplays between

  • Data consistency / single-source-ness demands that e.g. between two windows, there should only be primitive data exchanged, say str/int identifiers, and both have access to their repositories; not throwing loaded data sets around my memory in order to go stale at times
  • Comprehension, most significantly Single Level of Abstraction, or other indicators of mental load like how many levels of intendation / return paths are mixed within sight (and also, Type Annotations do help a lot in Python, even though not mandatory, i.e. the complete opposite of fighting Redux-TypeScript-chimaeras – but I digress…
  • Robustness, where I would believe that my user (me) has virtually no chance of even seeing this and that window when their data is maybe still loading somewhere – but I still check these cases, because this bedtime story has no business in leaving you an hopeful-to-anxious pile of nerves
  • Traceability of your state, for troubleshooting and useful UI feedback (as you’d guess, real-time event based stuff is not easily debugged by break points or logging alone).

So over months in that project, I grew annoyed of code like (Symbolbild)


class Editor:
    # ...
  
    def load_editor(self, params: Optional[EditorParams]):
        if params and (self._entity is not None or
                       self._entity.id != params.id):
            if entity := repository.load_entity(params.id):
                self._entity = entity
            else:
                raise ValueError("repository needs some alone time :(")
            self._entity.other_stuff = other_repo.check_stuff()
        elif params is None:
            raise TypeError(
                "sounds Optional in our signature, but actually is not"
            )
        elif self._entity.id == params.
            self.adjust_more_stuff(self._entity, params.stuff)
            # ...

Because encountering any single block of these drags you down, I have currently accustomed myself to write these as (one can argue whether the names like “Supplier” are the best here, but they’re not the worst, I believe)

@dataclass(frozen=True)
class LoadedEntity:
id: str
entity: Optional[Entity]
stuff: Optional[OtherStuff]
@property
def is_unusable(self):
return self.entity is None
@property
def missing_stuff(self):
if self.is_unusuable:
return True
else:
return self.stuff is None
class EntitySupplier:
_current: LoadedEntity
_entity_repo: EntityRepository
_stuff_repo: OtherStuffRepo
# __init__ etc. hereby left out as boilerplate
def load_params(self, params: Any):
# do all your checks in here
if (... very bad ...):
self._current = LoadedEntity(params.id, None)
return
entity = self._entity_repo.get(params.id)
stuff = self._stuff_repo.get(params.stuff).for(entity)
return LoadedEntity(
params.id,
entity,
stuff
)
@property
def entity(self):
return self._current.entity
def expecting(self, stuff: bool = False) -> Optional[LoadedEntity]:
if stuff and self._current.missing_stuff:
return None
return self._current
class Editor:
_supply: EntitySupplier
_logger: SomeLogger
def __init__(self, **kwargs):
self._supply = EntitySupplier(**kwargs)
self._logger = BlaBlaLogger()
def load(self, params):
self._supply.load_params(params)
if entity := self._supply.entity:
self.update_ui(entity)
else:
self._logger.error("Outsmarted, eh? %s | %s", str(params), stack_trace())
return
if supply := self._supply.expecting(stuff=True):
self.initiate_stuff_from(supply)
else:
self._logger.info("Entity %s is ready, Stuff is not | %s", str(entity), stack_trace())

So, the LoadedEntity serves like a concatenation of several Optional types, but it wraps the logic (i.e. there’s no sense in having stuff when you don’t have entity first) instead of just shruggingly claming “well, this entity here is optional – and that other stuff is, too”. Now, LoadedEntity is not a pretty name at all (have a better one?), but it sure beats having two straightaway lies.

I like that pattern because it allows me to stash the EntitySupplier and LoadedEntity somewhere on their own (I do strictly not believe that every class needs its own file, but some of the “Single …” ideas (Responsibility, Level of Abstraction, you name it) do also apply here; and the Editor.load(…) itself does read somewhat like a short story. It has quite linear structure and can early-return, and/or log, on demand, and while naming is still hard (consistently voted one half of famous Hard Things), I could even have some fun in designing that language while preserving the idea, that future-me can arrive in a few weeks (read: hours) and still trust in some of the entites and stuff.

The quintessence here is: Checking for None (which is Python’s NULL, and the typing Optional[T] is identically equal to T | None) is still a thing in 2026 due to its sheer practicality, but if you design some some structure around that and keep these checks in something like LoadedEntity, you can keep the abyss from staring back into you.

Rails Strict Locals: Giving Partials an Explicit Interface

Rails partials are a great way to reuse view code, but they have traditionally suffered from one weakness: their interface is implicit.

When opening a partial written by another developer, it is often unclear which locals are required, which are optional, and whether all of them are still used. IDEs typically cannot help much either, often showing warnings about unresolved variables because they cannot determine where the values come from.

The problem becomes even more apparent as an application grows and partials are rendered from multiple places.

If a local is forgotten by call, the error only appears when the template is rendered:

undefined local variable or method `missing_local'

If an extra local is passed, Rails traditionally ignores it.

Over time this creates a situation where the real API of the partial exists only in the heads of the developers maintaining it.

Rails Strict Locals

Rails provides a feature called strict locals that allows a partial to declare its expected interface:

<%# locals: (title:, highlight: false) %>

The declaration resembles Ruby keyword arguments and is placed at the top of the template.

A local like title without a default value is required. Locals like highlight with default values become optional

The partial now documents and enforce its own API. If a required local is missing, Rails raises an exception instead of failing later when the variable is accessed. Likewise, if a caller provides a local that is not declared, Rails rejects it.

Conclusion

Strict locals do not fundamentally change how partials work, but they make them easier to understand and maintain.

By declaring the expected locals directly in the template, partials become self-documenting and gain an explicit contract with their callers. Missing locals are detected early, obsolete locals are rejected, and developers no longer have to search through controllers, parent templates, and render calls to understand where variables come from.

An additional benefit is improved tooling support. Once the interface of a partial is explicit, IDEs can understand the available variables much better. Your IDE becomes a helpful companion again rather than a source of noise.

A doubly linked list for entt components

I recently implemented a small CRTP template to group entities in entt. It turned out quite nicely, so let me share it here:

template <class T> class doubly_linked_component
{
public:
using node_type = doubly_linked_component<T>;
static void on_construct(entt::registry& entities,
const entt::entity e)
{
auto& that = entities.get<T>(e);
that.next_ = that.prev_ = e;
}
static void on_destroy(entt::registry& entities,
const entt::entity e)
{
auto& that = entities.get<T>(e);
// List has only this element?
if (that.next_ == e)
{
return;
}
auto next = that.next_;
auto prev = that.prev_;
auto& next_node = static_cast<node_type&>(entities.get<T>(next));
auto& prev_node = static_cast<node_type&>(entities.get<T>(prev));
prev_node.next_ = next;
next_node.prev_ = prev;
}
static void merge(entt::registry& entities,
entt::entity lhs, entt::entity rhs)
{
auto& lhs_node = static_cast<node_type&>(entities.get_or_emplace<T>(lhs));
auto& rhs_node = static_cast<node_type&>(entities.get_or_emplace<T>(rhs));
// The end of left (which is left.prev_) needs to point to right
auto lhs_end = lhs_node.prev_;
auto rhs_end = rhs_node.prev_;
auto& lhs_end_node = static_cast<node_type&>(entities.get<T>(lhs_end));
auto& rhs_end_node = static_cast<node_type&>(entities.get<T>(rhs_end));
lhs_end_node.next_ = rhs;
rhs_node.prev_ = lhs_end;
rhs_end_node.next_ = lhs;
lhs_node.prev_ = rhs_end;
}
static std::generator<entt::entity> enumerate(entt::registry& entities,
entt::entity e)
{
// By default, entities are their own lists
if (!entities.any_of<T>(e))
{
co_yield e;
}
else
{
auto current = e;
while (true)
{
co_yield current;
current = entities.get<T>(current).next_;
if (current == e)
break;
}
}
}
private:
entt::entity prev_ = entt::null;
entt::entity next_ = entt::null;
};

You can use it like this:

struct cool_group : doubly_linked_component<cool_group> {};

By default, each entity represents its own 1-sized group. To merge two groups:

cool_group::merge(entities, left, right);

To iterate over all entities, I am using the new std::generator and coroutines. You can use it like this:

for (auto entity : cool_group::enumerate(entities, head))
do_something(entity);

entt will automatically call into on_construct and on_destroy.

People nowadays usually avoid linked lists because of all the pointer chasing required to actually use them. The pointer chasing is not the problem though, the non-locality is. If you make sure your nodes are all allocated in memory close to each other, there is hardly a penalty. entt will usually do this if nodes are also allocated close in time, which is often the case if you want to group things, so this works nicely in that regard, too. Feel free to use this code under CC0.

Avoiding Code Style Discussions

Every developer has personal formatting preferences.
Brace placement, line wrapping, imports, tabs vs. spaces — everybody has an opinion, and most of them are reasonable.

The problem starts when all these styles meet in one repository.

The cost of “personal style”

A codebase written by ten developers can easily look like ten different applications stitched together. Suddenly, pull requests are full of formatting changes. Git diffs become noisy. Merge conflicts appear because one developer reformatted a file differently than another. Code reviews drift into discussions about whitespaces instead of actual functionality.

Even worse: inconsistent code slows down reading.

Humans recognize patterns quickly. When code follows the same visual structure everywhere, the brain spends less effort parsing syntax and more effort understanding intent.

Consistent formatting reduces cognitive load.

A shared style is less about aesthetics and more about reducing friction. But how to solve this problem?

Shared Project Style

In IDEs like IntelliJ, you can define a code style and automatically reformat code according to those rules. This helps you keep your own code consistent. However, if every developer uses a different style, it does not help the project as a whole.

You can configure the style under:

Settings -> Editor -> Code Style

and save it as a project-level configuration. IntelliJ will then create a codeStyles folder with XML files inside the .idea directory.

The solution for sharing one configuration across the whole project is to commit these files to Git. This way, every developer working on the project uses the same code style configuration.

The IDE can then help enforce the agreed style by reformatting code before commit or even automatically on save.


Consistency beats preference

The important thing is not finding the perfect style. The important thing is agreeing on one.

A consistent codebase is easier to read, easier to review, and easier to maintain. Pull requests become smaller and cleaner because they contain actual changes instead of formatting noise.

Good formatting should be boring and automatic. That leaves more time for discussions that actually matter.

Consistent Structure Considered Harmful

Debating about pros and cons of different code styles quickly tend to enter “which color is best”-territory, so this is not what I’ll do here, but consider the following an internal debate of mine that occurs from time to time.

The “Structure” of a piece of code spans various topics: from syntactical preferences like conventions for curly braces, to case conventions, naming variables, indentation, line breaks and whitespace in general, how to distribute methods between classes, between files, up to its high-level architecture. I am strictly not talking about the higher levels here.

However, in the finer levels of your code, you will apply a mixture of conventional choices coming from the programming language, the culture surrounding it, and your own background. Some will be shaped by your IDE. Keep in mind that all choices are to be done to aim for one goal:

To produce readable code which is straightforward to argue about.

“Readable” is a heavy word as it bears the inseparability of both (a) prerequisites the reader has to fulfill (who are they and why are they even reading my code?) and (b) that getting used to one convention can greatly affect the reading speed more than any in-grained perks of that convention, but nevertheless, there does exist a dimension outside that.

There is some remaining variability in choice, for example, in how to continue the intendation in a multi-line argument list, how to place newlines in chained calls like fluent interfaces / LINQ in .NET / Promises in JS / … or in chained conditionals – the list goes on.

But for example; what is the optimum, e.g. in this Python example

if (self.evaluate_user_input()
and user_role is in (UserRole.Admin, UserRole.ProjectOwner)):
do_stuff()
# vs
if (self.evaluate_user_input() and
user_role is in (UserRole.Admin, UserRole.ProjectOwner)
):
do_stuff()
# vs
if (self.evaluate_user_input()
and user_role is in (UserRole.Admin, UserRole.ProjectOwner)):
do_stuff()
# vs
if (self.evaluate_user_input()
and user_role is in (
UserRole.Admin,
UserRole.ProjectOwner
)):
do_stuff()
# vs ... outsourcing any of that logic into its own place,
# but even that comes with risks of cluttering structure elsewhere.

Another example are braces in any language that uses them, because I’ve encountered a couple of scenarios where the convention would suggest…

if (theThing) {
quiteALot();
ofDifferent();
lines();
} else {
nowSomeCompletely();
differentStuffToDo();
}
# breaking the brace convention, just for that "else",
# conveys a lot purpose to distinguish these branches. for me.
if (theThing) {
quiteALot();
ofDifferent();
lines();
}
else {
nowSomeCompletely();
differentStuffToDo();
}

Line breaks and continuation can be crucial because they influence how far the eyes have to extend to the right (maybe requiring horizontal scrolling, which is a dealbreaker in any measure of quickly-understanding), but whitespace can be beneficial in distinguishing your product from a pile of unicode vomit (which is why gofmt is wrong in believing that inline formulae are always better with any spaces distilled out).

The more I think about it, the less I would agree with anyone convinced that one should just strife towards one certain style and then stick to it. Moreover, the actual content of a line of code can dominate the decision at hand more than a well-meaning thought of “all of these decisions are equal, do not waste any time about them”. The point is, that time saved in reading this can outweigh your time saved in not caring about said reader.

Of course, the title of this post was chosen somewhat demandingly because the actual goal with that is consistent code. The point in such situations being, that uniform guide lines do not automatically lead to consistent expression of intention.

Aim for what your specific piece of code needs to convey, then allow the idea of choosing a style that is not the same choice as for difference pieces of code. Do not overthink it either, but do not think that deviances in uniformity are a code smell.

A more variable, more purposeful coding styles is likely to to conflict when more than one developer is involved (because of the accustomization effect), but treat it like any performance optimization – discuss it when your Merge Review is actually troublesome, with intention, not miles ahead for some hypothetical horror scenario.