Where the Compiler Stops
Google recently published a piece titled "Why Go is an ideal language for AI-assisted software engineering." I recommend giving it a read. It makes a compelling, practical point that anyone building production systems with LLMs eventually collides with: when machines write code, the primary bottleneck shifts from writability to verification.
Python is unmatched for human prototyping, but in an automated generation loop, dynamic typing and implicit runtime state are liabilities. Go solves this by wrapping the model in a tight, deterministic feedback loop: fast compilation, rigid formatting (gofmt), low syntactic variance, and static type checks. The compiler acts as an automated sanity check before code ever reaches a human reviewer.
Google’s thesis is dead on. Static compilers and standardized formatting give models something to be checked against. If compiler feedback is where verification happens, the next question is how much a compiler can verify.
Go uses its compiler and static type system to enforce syntactic and structural correctness. This is where language preference has concrete ramifications. In an agentic coding loop, the compiler is effectively another source of context. Every rule it understands is a rule the LLM does not have to remember from a prompt, recover from documentation, or rediscover through a failing test. The richer the domain encoded in the type system, the richer the deterministic feedback returned to the model when it gets that domain wrong.
Business Logic as Compilation Correctness
Compilers check if the code can run, while dynamic validation, tests, and human discipline check if the code does what the business intended. A model writing code produces syntax that's valid and rules that aren't yours. Runtime validation catches that on the day it runs in production, assuming someone wrote the check for a case nobody anticipated.
In a well-designed domain model in a language with an algebraic type system (like F#, Rust, Swift, or OCaml), the business rules and the type definitions are the same artifact. An operation the type model makes illegal won't compile, so the check happens before the code runs rather than in a test someone remembered to write.
Put the rules in the types and the guards move out of the function bodies. A payment that requires human sign-off doesn't get an if (!isApproved) check inside the execution path. ExecutePayment takes an ApprovedOrder, and the only way to construct one is through the authorization step. An agent asked to execute an unapproved payment writes code that won't build.
The same applies to the small stuff. TenantId is TenantId of Guid, so passing a UserId where a tenant is expected fails at compile time. In a dynamic language that's a silent authorization bug that ships and gets found by a customer.
Correctness stops being something you remind the model about in a prompt and becomes the only shape the compiler will accept.
Why LLMs Naturally Reason About Algebraic Data Types
LLMs are language models. People ask them to do arithmetic and produce exact byte sequences and then act surprised when the output drifts. They are experts at language, and a domain expressed as language is home ground.
When you ask an LLM to generate code in an imperative or class-heavy paradigm, it has to navigate deep inheritance hierarchies, factory pattern boilerplate, and implicit mutable state. This consumes context window capacity on plumbing rather than domain logic.
Algebraic Data Types, specifically Discriminated Unions (DUs) and single-case domain types, mirror natural language specifications far more cleanly.
Consider how an order processing domain is defined in F#:
type OrderArtifact =
| OrderSubmitted of SubmittedOrder
| InsufficiencyDetected of InsufficiencyDetails
| ActionProposed of ProposedAction
| ExecutionCompleted of ExecutionReceiptOr similarly in Swift:
enum OrderArtifact {
case orderSubmitted(SubmittedOrder)
case insufficiencyDetected(InsufficiencyDetails)
case actionProposed(ProposedAction)
case executionCompleted(ExecutionReceipt)
}The code doubles as a specification a domain expert can read. Every state has a name the compiler knows about. Pending approval is a case in a union rather than a string someone has to spell correctly in eleven places. Values arrive fully constructed, so runtime null checks fall away, and anything coming out of a lookup already has a type by the time you touch it.
Compiler Errors as Prompts
In an agentic coding loop, compiler errors aren't merely diagnostics for a human developer. They are prompts generated deterministically by the program the model is trying to write. A compiler that understands only syntax reports malformed syntax. One that understands the states and transitions of a domain reports an implementation that doesn't cover them. The more meaning we move into the type system, the more meaningful the repair signal becomes.
If an LLM generates a Go snippet with a typo or structural type mismatch, go build provides a clean error message that the model can use to repair its output. But an ADT-driven compiler can put domain vocabulary directly into that feedback:
warning FS0025: Incomplete pattern matches on this expression. For example, the value 'InsufficiencyDetected' may indicate a case not covered by the pattern(s).
Treat warnings as errors and the agent cannot declare success without accounting for InsufficiencyDetected. This is importantly different from telling the model in a prompt that insufficiency is possible. Prompt instructions are context the model is expected to remember. The compiler error is evidence that the artifact it produced does not satisfy the domain model. The model generates code, the compiler checks it against the domain model, and any failures become context for the next attempt.
As an aside, F# has FSI. An agent can load a domain model into the REPL and exercise it against real values without wiring any of it into an application. It doesn't have to build a project to find out whether its own design holds. Compilation says the code is consistent. FSI lets the agent interrogate that design before it writes the surrounding implementation.
Where the Compiler Stops
Language choice in AI-assisted software engineering goes beyond the question of which language is easiest for a model to generate. What matters is what deterministic machinery surrounds the model after it generates something. Algebraic domain modeling lets us push more into that machinery.
If TenantId and UserId are different types, the compiler can reject their substitution. If an operation requires an ApprovedOrder, the compiler can reject attempts to execute a merely SubmittedOrder. If InsufficiencyDetected is a possible state, exhaustive pattern matching can reject code that pretends it doesn't exist.
With the business modeled correctly, the LLM writing the code no longer has to independently remember every rule we encoded. The compiler remembers it for us. Fast compilation makes a good repair loop for generated code, and rich domain types make the feedback from that loop increasingly about the program we meant to build.
The question, then, is how much meaning we can put into compilation.