Immutability Is Why LLMs Reason About Elixir Better
The single biggest reason large language models write Elixir well is not the syntax or the ecosystem. It is immutability. Once you see how immutability changes what a model has to keep track of, the benchmark results stop looking surprising. This post takes one mechanism apart in detail: why data that never changes underneath you is easier for a model to reason about, and why that shows up as fewer bugs in generated code.
What immutability actually means
In Elixir, values do not change in place. When you “update” a map, you get a new map back and the original is untouched. A function cannot reach into a data structure you handed it and quietly rewrite it. What goes in is what goes in, and the only thing that comes out is the return value.
list = [1, 2, 3]
_shorter = List.delete(list, 2)
list
# still [1, 2, 3]Contrast that with a mutable language, where a method call on an object can change that object, change a global, or change something three layers up the call stack, and none of it is visible at the line where you made the call.
The hidden-mutation problem
In mutable code, to know what process(order) does, you cannot only read process. It might rewrite order. It might update a shared cache. It might touch a field another thread is also touching. To be sure, you have to read process, and everything process calls, and hold the whole chain in your head at once. That is what people mean by “spooky action at a distance”: effects that land somewhere other than where you are looking.
Humans cope with this by building a mental model of the system and keeping it warm. It is slow and error-prone, but we manage.
The bug this breeds is familiar to anyone who has written mutable code. You pass a list into a helper to “check” it, the helper sorts it in place as a side effect, and now the caller’s list is silently reordered. Nothing at the call site says the helper mutates its argument, so the reader, human or model, has no reason to suspect it. In Elixir that bug cannot be written: the helper receives a value, and whatever it does internally, the caller’s list is exactly what it was. The failure mode is closed off at the language level rather than left as a trap for the reader to remember.
Why it is worse for a model, and why immutability fixes it
A language model does not carry a warm mental model of your codebase. It has a context window: a fixed budget of text it can look at right now. Every function it must read to be sure about a side effect is text it has to pull into that budget, and the budget runs out. Mutable code forces the model to widen its view, and a wider view means more tokens, more chances to lose the thread, and more chances to guess wrong about an effect it cannot see.
Immutability collapses that problem into local reasoning. To understand an Elixir function, you read the function. Its inputs are its arguments, its output is its return value, and nothing else moved. The model needs less surrounding context to be correct, which is another way of saying it has fewer openings to be wrong. On a benchmark graded by whether the code actually runs, fewer openings to be wrong is exactly what moves the score.
The pipe operator makes the data flow visible
Because functions transform data instead of mutating it, Elixir can chain them into a single readable pipeline. Each step takes the previous result and returns the next:
"the quick brown fox"
|> String.split()
|> Enum.map(&String.length/1)
|> Enum.sum()You can read that top to bottom and know precisely what the data looks like at every stage. Nothing is hiding. A model reading it has the same advantage: the transformation is on the page, not buried in the state of some object.
Contexts carry the idea up to the module level
The same property scales past single functions. A Phoenix context is a bounded module with a clear public interface, and I made the fuller case for it in why Phoenix contexts are great for LLMs. The point that matters here is that a well-drawn context is a small, self-contained surface. A model can reason about it without loading the rest of the application, for the same reason it can reason about an immutable function without loading the rest of the call stack.
It also removes a whole category of bug
A second payoff matters as much as readability. If no code can mutate shared data, then two things running at the same time cannot corrupt each other’s state, because no shared mutable state exists to corrupt. Race conditions, the class of bug where the answer depends on which thread got there first, mostly evaporate. That is a large part of why the BEAM can run hundreds of thousands of concurrent processes without the locking machinery other runtimes need.
For a coding agent, this closes off an entire family of mistakes it would otherwise have to reason about and usually gets wrong. Concurrency bugs are exactly the kind of error that does not show up in a quick read of a single function; they emerge from timing and interleaving across the system, the hardest thing for a model with a bounded context window to hold in view. Immutability means the agent mostly does not have to. The bug it cannot see is a bug the language already prevented.
What it costs
Immutability is not free, and pretending otherwise would be dishonest. Building a new value instead of mutating one in place can cost memory and time, though the BEAM softens that by sharing structure between old and new versions under the hood. Some algorithms are easier to express with mutation. And developers arriving from object-oriented languages have habits to unlearn. These are genuine trade-offs.
The claim is not that immutability is free. The claim is that it makes code easier to reason about, and that a coding agent, with no persistent memory of your system and a hard limit on what it can read at once, benefits from that even more than a human does.
Why it matters if you build with AI
Fewer hidden effects means fewer of the nastiest bugs, the ones where generated code looks correct and quietly does the wrong thing to shared state. It does not remove them. A model can still write a function that is locally sensible and globally wrong. That residue is why a written specification to aim at, and a verification pass to catch what slips through, still earn their place on top of a good language. Immutability gives the agent a better draft. It does not hand you a finished, correct application. CodeMySpec is built for the gap between those two.
Related Articles
- Why Elixir Is the Best Language for LLMs
- Why Phoenix Contexts Are Great for LLMs
- AutoCodeBench Explained
Sources
- José Valim / Dashbit, “Why Elixir is the Best Language for AI”: https://dashbit.co/blog/why-elixir-best-language-for-ai
- AutoCodeBench project page: https://autocodebench.github.io/
- AutoCodeBench paper: https://arxiv.org/abs/2508.09101