Why LLMs can't make your code simpler

tosh1 pts0 comments

Why LLMs can’t make your code simpler – Answer.AI

This post was originally published on Medium.

tl;dr Peter Naur’s “Programming as Theory building” states that the real programs, what he calls Theory, with capital T, are in the mind of the engineers. The code & documentation are just downstream (and thus incomplete) artifacts. One of my main complaints about LLMs is how verbose the code is and how complexity creeps in everywhere. I always had some faith that we might improve that using metrics like LoC or the number of independent code paths to constrain them. However, after reading Naur I realized that the complexity we are trying to reduce is the Theory one, not the code, and there are no relevant measures for that because it’s very subjective.

Peter Naur

I recently read the fantastic paper “Programming as Theory building” by Peter Naur. It has changed quite substantially my views on what current LLMs can or can’t do, how to prompt them, what are the main limits to current agentic systems or why pair programming is so effective.

Today I will focus on its relation to code complexity.

If you haven’t read the paper, I really suggest that you do. In fact, my main goal in writing this post is to get some of you to read the original paper. It is worth the effort. It is also the perfect chance to practice (or learn!) to do close reading in Solveit because it’s much easier: you can ask any questions throughout the text, go down any rabbit holes you like, or just ask Solveit to make the language clearer to you. More information on close reading in this blogpost and you can quickly get started by forking my own dialog.

With that in mind, if you still decide not to read it, here’s the paper’s tl;dr:

A program is the Theory held by the people who build and maintain it: an understanding of how the program relates to the real-world problem, which constraints and trade-offs shaped it, why it works, and which changes would fit its design. Code and documentation are downstream artifacts of that Theory, and can never capture it completely.

Engineers develop this understanding through experience: talking to users, observing failures, learning the domain, and seeing how the system behaves in the real world. This understanding guides judgments of relevance, similarity, simplicity, and good design.

LLMs aren’t great at continual learning, talking to users, or experiencing things in the real world. But why is this relevant to complexity?

I think we can all agree that LLMs in general tend to increase the complexity of codebases if left unchecked. The reasons are many: they fail to realize a method already existed and duplicate it, they write overdefensive code like protecting against edge cases that can’t happen, or overoptimizing things too early. Tangentially, most frontier labs make a hefty sum the more tokens you consume, so they are kind of incentivized to promote tokenmaxxing. All in all, LLMs rarely follow the KISS principle. This issue is at its worst when vibe-coding w/o checking the output. But even when you review code, if you want to keep a program concise it requires an active effort to cut down complexity as much as possible.

In my naive days (about two weeks ago), I used to think we would get out of this complexity pit at some point. Frontier labs would just add some complexity penalization to their RL training. They could use total LoC as a metric to minimize, but we can all agree that sometimes a one-liner can be more complex than 2–3 lines. Another option would be cyclomatic complexity which measures the total number of independent code paths. After reading Peter Naur I realized that those things can’t really solve the problem (maybe they can alleviate it a bit though). Let’s see why.

Why Code Complexity is the wrong metric

In the (invented) example below we want to support calling OpenAI & Anthropic. Imagine all the message preparation and retries are handled identically, but the request body params are slightly different so we have 2 different methods call_openai and call_anthropic.

In the first naive version we have 2 different classes with duplicated boilerplate code (ie prepare and with_retries).

# Separate — duplication a metric would flag

class OpenAIClient:<br>def complete(self, prompt):<br>msgs = prepare(prompt) # shared boilerplate<br>y = call_openai(msgs) # the only line that differs<br>return with_retries(y) # shared boilerplate

class AnthropicClient:<br>def complete(self, prompt):<br>msgs = prepare(prompt)<br>y = call_anthropic(msgs)<br>return with_retries(y)

A straightforward refactor would be to create a single class so we DRY. By many metrics this is a better implementation: it has fewer lines of code, better Halstead volume — calculated as V=N×log2(n) for program length (N) and vocabulary (n) — and better Maintainability Index.

# Merged — genuinely better by the metrics: DRY, fewer lines

class LLMClient:<br>def __init__(self, provider): self.provider = provider<br>def complete(self, prompt):<br>msgs =...

code complexity llms theory prompt naur

Related Articles