Why AI Coding Agents Love Elixir (And You Should Too)
Immutability, pattern matching, and pure functions make Elixir ideal for AI coding agents—the data proves it
Elixir scored 97.5% in Tencent's AI coding benchmark. That's not a typo; that's the percentage of Elixir problems that at least one AI model could solve—the highest among twenty programming languages tested. Claude Opus 4 alone hit 80.3% on Elixir tasks; first place among all models, beating C# at 74.9% and Kotlin at 72.5%.
These numbers come from AutoCodeBench, a study published by Tencent's AI team in late 2025. The benchmark contains 3,920 problems distributed evenly across twenty languages, from mainstream options like Python and Java to smaller ecosystems like Elixir, Ruby, and Scala.autocodebench-methodology Somehow, the functional language with a fraction of Python's market share outperformed everything else.
I've been using Elixir for years; I've also spent the past eighteen months pairing with AI coding agents almost daily. The benchmark results didn't surprise me. They confirmed something I'd been noticing in practice: AI agents are remarkably good at reading, understanding, and modifying Elixir code.
Not coincidence. The language was designed with constraints that align—almost accidentally—with how large language models reason about code.
The Tencent Study in Context
AutoCodeBench tests whether AI models can complete code given a function signature and docstring; it measures the model's ability to understand intent and produce correct implementations. The 97.5% figure represents the union of all thirty-plus models evaluated—at least one model solved almost every Elixir problem.union-metric
The individual model performance is where it gets revealing. Claude Opus 4 in reasoning mode achieved 80.3% on Elixir, its strongest performance across all languages. The pattern held in non-reasoning mode and for Sonnet as well. AI models consistently perform better on Elixir than on languages with ten times the training data.
Why?
José Valim, Elixir's creator, published an analysis exploring this. His hypothesis centers on what he calls "local reasoning"—the ability to understand a function without tracing execution through a labyrinth of hidden state. Elixir's constraints make this possible by default; most languages make it difficult or impossible.
Here's the thing: AI models don't read code the way humans do. They process token sequences and predict likely continuations based on patterns learned from training data. When those patterns are consistent and self-contained, prediction becomes more reliable. When understanding a function requires tracing mutable state through multiple files and implicit dependencies, the probability distribution spreads thin.
Elixir's design concentrates that probability.
Immutability as Cognitive Scaffolding
An AI agent encounters this Python code:
def process_user(user):
validate(user)
enrich(user)
return save(user)
To understand what process_user returns, the agent needs to trace what validate, enrich, and save do to the user object. Each function might mutate it. Each might mutate shared state. Each might have side effects that influence later calls. The agent has to reason about order, about hidden dependencies, about what user looks like after each transformation.
The Elixir version:
def process_user(user) do
user
|> validate()
|> enrich()
|> save()
end
The structure looks similar. The semantics are completely different.pipe-operator
Each function receives a value and returns a new value. Nothing mutates; validate/1 can't change user because data in Elixir is immutable. The agent knows exactly what each function receives—whatever the previous function returned. No hidden state. No spooky action at a distance.
This matters computationally, not just aesthetically. The input-output relationship of each function is explicit in the code itself; an AI agent can analyze validate/1 in complete isolation without worrying about context it hasn't seen.
Valim's framing of "local reasoning" captures it well: anything a function needs must be given as input; anything a function changes must be returned as output. An AI model reading Elixir code can predict function behavior from the function itself. In mutable languages, that prediction requires simulating the entire call stack—a much harder problem for a statistical model.
I've watched AI agents struggle with Ruby classes that accumulate instance variables across method calls. The same agents handle equivalent Elixir structs with ease. Not because Elixir is "simpler" in some absolute sense—it isn't—but because the state dependencies are encoded explicitly in the signatures rather than hidden in the runtime.
Pattern Matching: Code That Documents Itself
Pattern matching encodes the contract in the function head itself. No guessing.
def handle_response({:ok, %{status: 200, body: body}}) do
{:success, Jason.decode!(body)}
end
def handle_response({:ok, %{status: status}}) when status >= 400 do
{:error, :http_error, status}
end
def handle_response({:error, reason}) do
{:error, :request_failed, reason}
end
An AI agent reading this code immediately knows: responses come as either {:ok, map} or {:error, reason}. Success means status 200. Client/server errors have status >= 400. Each case produces a specific output shape.
This is documentation that can't drift from implementation. The pattern match is the type contract; you can't call these functions with inputs that don't match without triggering a FunctionClauseError.function-clause-error An AI agent can rely on these constraints as ground truth.
The equivalent in Python:
def handle_response(response):
if response.ok:
if response.status_code == 200:
return ("success", response.json())
elif response.status_code >= 400:
return ("error", "http_error", response.status_code)
else:
return ("error", "request_failed", str(response.error))
Same logic. But the AI agent now has to parse conditional branches to understand the protocol. It has to trust that response has .ok, .status_code, and potentially .error attributes; the valid input space is implicit in the conditionals rather than explicit in the signature.
Pattern matching collapses possible interpretations. When an AI sees {:ok, result} or {:error, reason} tuples, it's seeing a standardized protocol that appears throughout the Elixir ecosystem—these patterns become high-probability tokens in the model's prediction space.training-corpus-opacity The agent knows what comes next because it's seen this shape thousands of times in training data.
The with construct pushes this even further:
def create_order(params) do
with {:ok, user} <- fetch_user(params.user_id),
{:ok, product} <- fetch_product(params.product_id),
{:ok, order} <- Order.create(user, product) do
{:ok, order}
else
{:error, :user_not_found} -> {:error, "Unknown user"}
{:error, :product_not_found} -> {:error, "Unknown product"}
{:error, changeset} -> {:error, format_errors(changeset)}
end
end
Every success case matches {:ok, value}. Every error case is enumerated explicitly. An AI agent generating code that calls create_order/1 knows exactly what shapes to handle. Self-documenting protocol.
The Documentation Factor
Elixir gives AI agents something rare: executable examples they can test their understanding against.
@doc """
Splits a string on occurrences of the given pattern.
## Examples
iex> String.split("a,b,c", ",")
["a", "b", "c"]
iex> String.split("hello world", " ")
["hello", "world"]
iex> String.split("no match", "x")
["no match"]
"""
@spec split(String.t(), String.pattern()) :: [String.t()]
def split(string, pattern) do
# implementation
end
Those iex> lines aren't just documentation—they're tested as part of the library's test suite via ExUnit's doctest feature.doctest-origin An AI agent can trust that String.split("a,b,c", ",") actually returns ["a", "b", "c"] because the test suite verifies it.
@spec annotations add a second signal. Type specifications in Elixir are optional but widely used; they describe input and output types in a format that tools—and AI agents—can parse mechanically. String.t() input, String.pattern() pattern, list of String.t() output. No ambiguity.
HexDocs centralizes all of this. Every published Elixir package has documentation at hexdocs.pm/package_name, generated automatically from the source code's @doc and @moduledoc attributes; AI agents trained on web data have encountered consistent, structured documentation for virtually every library in the ecosystem.
The ecosystem's stability makes this even more durable. Elixir hit v1.0 in 2014 and is still on v1.x today; Phoenix reached v1.0 the same year and sits at v1.8 with no breaking changes in the core API. Ecto has been on v3 since 2018.elixir-semver-philosophy An AI model trained on Elixir code from 2020 will still produce valid code in 2026.
That's not true for most ecosystems. JavaScript frameworks churn constantly; Python's packaging story has fragmented across pip, poetry, conda, and uv; Ruby gems break between minor versions. AI agents trained on stale data produce stale code. Elixir's commitment to stability means training data stays relevant longer—a quiet advantage that doesn't show up in any benchmark.
The Workflow in Practice
What does this look like day-to-day?
Dashbit released Tidewave, an MCP server that exposes your running Elixir application to AI agents.mcp-protocol The agent can introspect process state, query ETS tables, inspect supervision trees—the kind of runtime information that's typically opaque. The BEAM's observability, built decades ago for telecom reliability, becomes an AI capability.beam-observability
# With Tidewave, an AI agent can:
# - List running processes and their states
# - Query the contents of ETS tables
# - Trace function calls in real-time
# - Inspect supervision tree structure
In practice, the difference is noticeable. When I ask Claude to refactor an Elixir module, it gets the pattern matching right. It understands that GenServer callbacks have specific return shape requirements. It suggests with blocks for error handling rather than nested conditionals. The suggestions feel idiomatic in a way that AI-generated Python or JavaScript often doesn't.
AI agents aren't perfect at Elixir—they still hallucinate functions that don't exist; they sometimes propose libraries that were deprecated years ago; they occasionally generate code that type-checks but violates implicit invariants. The 97.5% isn't 100%. But the gap between "works most of the time" and "works sometimes" is the gap between useful tool and frustrating distraction. Elixir sits on the right side of that divide, and the language's design constraints are why.
The benchmark numbers are interesting; the underlying explanation matters more. Languages designed around explicit state, pure functions, and self-documenting patterns happen to be languages that AI models can reason about well. That's not an accident—it's what happens when language design principles and statistical prediction mechanics point in the same direction.
Elixir wasn't designed for AI agents. It was designed for human programmers who wanted to reason about concurrent systems without losing their minds. Immutability, pattern matching, explicit dependencies—the same properties that help humans understand Elixir code help AI models understand it too.
I keep coming back to something Valim wrote in his analysis: the best code for AI is also the best code for humans. That's either a reassuring coincidence or an obvious inevitability, depending on your philosophical disposition. Either way, as AI assistance becomes a larger part of how we write software, the languages that made correctness easy are turning out to be the languages that made AI assistance effective. I'm not sure anyone planned for that.