Higher level classes

I'm just emerging from a bit of a nightmare related to higher level classes, hence this cautionary note. It‘s particularly relevant if you are coding with LLMs.

The crux of it is quite simple. I had a need to create some decorators, which is a way of dynamically adding new behaviors or responsibilities to an object or class without altering its underlying code. It is a quintessential example of composition over inheritance: instead of creating a rigid, deep subclass hierarchy to add a feature, you wrap the target with a decorator that intercepts or enhances its functionality. At it's core it's just a function that takes the class, method or property as input and wraps it with extra logic, and returns the enhanced version.

They are referred to as higher order function, or loosely as "higher-level" because they are functions that take other functions as arguments or return them. The annoying thing about higher-level functions is that they are tricky to reason about. Taken on their own, not so much, but when they are surrounded by a much bigger codebase, the level of complexity you have to hold in your head gets gnarly fast. But they can be very useful, and in some circumstances it's literally the only way of achieving what you want.

I have used decorators a lot before, in fact in nodejs you are using small versions of these all the time, but I hadn't had to use them to decorate classes before, and that is what the LLM I was working with suggested. As we were tackling a quite tricky thing, it made the case for using some class decorators. It made a lot of sense, and quite quickly we were off to the races. Of course I reviewed the code, but I didn't dig too much into the detail and I certainly didn't think about the longer term consequences.

As if often then case with software, as we continued to work on the feature, we discovered that the initial solution, and in fact the architecture it was embedded in, needed to change, and we set about doing that. This is where things started going off the rails, and at first I didn't notice. In my head it seemed that the higher order function solution was quite self contained, but as we re-architected the code, the LLM re-architected the higher order classes implementation, and in retrospect, it clearly got a bit confused, or just didn't think it through properly.

At a certain point I realised that the LLM had added several layers of indirection, which wasn't totally obvious because it was spread across several files in very different locations, and when I asked about these layers, it had very good arguments for them. They are acting as "runtime binding factories" was one that initially sounded like it could be a real thing that was needed. You have data at runtime that you need injecting into the class you are decorating, so of course you need a runtime binding factory. Duh. Yet somehow the more I asked about it, the more the explanations seemed contrived.

I eventually paused everything and dug into it myself, and painstakingly traced the code through and discovered all these horrific levels of totally unnecessary complexity, with very confusing function names, and to make it worse it had been importing various things and stuffing them inside closures at every level. A real tangled mess. Yet somehow it sort of worked, until you tested it properly, or refactored it slightly and it all fell apart.

I started by trying to rename all the function names to names that actually made sense. Often these ended up being ridiculously long names, which in itself was a sign things were not right. Anyway doing that enabled me to point out how unnecessary much of the cruft was, and soon enough we were able to drastically reduce the number of useless levels of complexity and unnecessary imports and closures.

All that said, the final solution is very neat, and actually not that complex. It's a little bit more complex than normal code, but not that much, and it's only in a very well understood place, and done in a repeatable way. The point is that if you are writing higher order functions with LLMs, it's worth slowing down and really make sure you understand the logic because it gets gnarly fast and the LLMs are unbelievably good at creating a giant knot out of everything and then telling you how great it is.