LLMs Will Cheese Your Types: Fighting Back in Haskell · in Code
Contents
Sooo yes it’s true, I’ve been integrating LLMs and agentic coding tools in my Haskell coding since the beginning of this year for a lot of my projects, both personal and professional. I do all of my programming in Haskell, a language with a very expressive type system that encourages “type-driven development”.
Working with LLMs on writing Haskell is a very unique experience; a lot of similarities and patterns exist with “normal” agentic coding, but I think the hot flame of LLMs meeting the cool stone of Haskell yields a lot of wholly unique optimal paths, workflow quirks, and failure modes.
This post will focus on understanding what I call constraint-evading behavior in LLMs as it relates to writing Haskell effectively with LLM collaboration.
Consider this Part 1 of a series. This post is about how to spot and understand a very common failure mode of LLMs once you actually are writing “type-driven” Haskell.
The Ideal Case: Haskell and LLMs
Now, my personal opinion and wishful hope is that Haskell should be to agentic software engineering what Lean is in agentic research mathematics: a framework for LLMs to self-construct the scaffolding they need to guide themselves to their correct goal.
I don’t believe that “correctness at generation-time” is a plausible goal, not today in 2026, and probably not any time soon. Motion towards correctness is asymptotic. You might get close enough sometimes, but the long tail of correctness is…long. Even the most full-vibed frontier-model projects have large suites of tests that exist to guide the agent. Agentic coding in five years will not be “spit out the correct program”, it will be “set up the best scaffolding that guides the implementation”.
Remember: the point of types in Haskell isn’t to catch bad code. It’s to direct how you write and structure your code, guide you down the most productive paths, help you concretely iterate on what design you want, and make the actual code-writing time a smooth flow process.
Each part of this is antithetical to how LLMs are trained to use types and write code.
To this end, I try to structure my codebases with the correct scaffolding to help augment the intuition of path exploration: AI has to search which paths are the “most promising”, so I structure my code-base to quickly kill off paths that are most likely to be dead-ends or lead to unmaintainable code, and to channel AI exploration along more promising paths. The greatest tools are types, compilers, warnings, hints.
A lot of these are the same things we teach to human coders, and are not very different than what I write about regularly:
Parse, Don’t Validate: set up your types to make invalid states unrepresentable. AI will, by nature, NOT do this, even the latest frontier models. They usually slip into defensive programming (adding isNull checks everywhere, boolean checks for constructors like isNothing / isObject / isArray instead of just pattern matching, filtering lists for duplicates instead of using Data.Set, adding precondition assertions, etc.), and it’s usually up to the human driver to stop and consciously create data types that model the domain correctly, structurally prevent invalid states, enforce pre- and post-conditions via structurally verified types and not boolean checks, avoid boolean blindness, etc.
Either that, or dedicate an entire session or planning step to clarifying these domain requirements in their types.
Use warnings and linters enthusiastically: -Werror=incomplete-patterns of course, and the default -Werror usually covers a lot of AI failure modes (leaving in dead code, leaving in arguments in functions for no reason and killing opportunities for abstraction).
Add robust test suites for things that cannot be tested within the types, but also explicitly laying out integration tests: something which LLMs seem pretty allergic to without proper prompting.
From my own empirical observations, all of these things are against the nature of how even frontier LLM models operate, embedded deeply from being trained on terabytes of untyped Python and React slop.
So, I’ve been gathering a list of what I call constraint-evading behavior : when the requirements and constraints are explicitly and unambiguously stated, but LLM nature desperately tries to circumvent them because they cannot keep up a sustained fight against their deepest base impulses.
Decisions that require scrutiny
There is a class of failure modes where AI makes a risky decision that a human might reasonably make in the rare case that it is justified. But, usually, it will be making this decision because it’s the “simplest approach”. It cannot separate questionable decisions based on reasoned justification and questionable decisions based on flawed heuristics like “simplicity” or effort.
We have the general failure modes like this that people mention for all programming languages:
“This test doesn’t...