Wow. Not a Haskell user, but a big user of other languages with expressive type systems (mostly Scala; some Rust). My experience is the complete opposite. I can't imagine using a language without a good type system to catch all the junk the LLM produces. In fact I thought people would move away from languages from poor type systems, like Python, given the cost of using languages with expressive type systems has decreased with LLMs.
IME Python has been very pleasant to use with types, even though they are not nearly as expressive as Haskell. I've noticed a shift in my own work where I spend more time playing with/manipulating change than I do making sure things type check. That does happen, of course, but it happens with less frequency then when I was writing Haskell by hand. During that time, I'd have stack running tests on file change and it was pretty smooth as well, but that workflow breaks down a bit with the current generation of agent harnesses we have.
I'm pretty sure that's the general trend and it will continue.
But I do think what benefits LLMs is the speed and accuracy of feedback. Type systems cover the accuracy part, but haskell was killing them on speed. It seems like a strange choice to go so far the other way on accuracy when there's a lot of languages in between. But I'm not familiar with the project so not in a position to call it.
It's not also really about expressiveness IMO. I've found LLMs to be best with more constrained type systems: they are better at ocaml than they are at typescript.
Java can also have 15 minute cold compiles on large projects if you kill all caches. It's less bad on smaller codebases because you don't have to recompile dependencies if you target a bytecode vm, but you just aren't gonna beat an interpreted language
But I'd look at people a bit oddly when they said: 'We didn't want to set up CI caching and compiled languages took 30 minutes per run so we changed our entire codebase to python'. Maybe it makes sense for them, and caching across dynamically spawned VM's is admittedly a harder problem which most build systems aren't great at, but still a very large jump
javac doesn't really do a whole lot. Consequently, whatever compile time you are complaining about would be worse with any other compiled language. Most optimization work in Java happens at runtime.
Secondly, there are several ways how Java source code becomes machine code, depending on which JVM and JDK is being used, not taking into account the ART cousin.
> I've found LLMs to be best with more constrained type systems: they are better at ocaml than they are at typescript.
When the potential set of behaviors you could write a program to have is infinite, but the actual behavior you want is singular, a programming language is more importantly defined by which ones it eliminates up front than which ones it lets you write (assuming it lets you write the one you want at all, but that's almost always going to be the case for most general purpose languages). Bugs are just false positives in this framing, where the program you wrote seems like the one you wanted, but there's some divergence between what you thought you were getting and what you actually got, and catching some of those up front is a huge part of why type systems are so useful.
exactly, i find the article a wierd take. i would have thougt thst being able to catch errors at compile time is the assurance that the LLM generated code is actually decent.
so does this mean that the LLM writes code that is so good that the compiler does not find any more errors?
or is it due to the nature of haskell that makes it hard to write bad code to begin with?
or just that because the haskell compiler catches more errors there is less broken haskell code of the AI to train on?
and what does that mean for the switch to python? if the python compiler/interpreter doesn't catch as many errors do we even know that the code is good?
or is this more like the belief if the LLM cab generate good haskell code, surely it can also generate good python?
what's the solution here? speeding up the haskell compiler? if that were easy, would it not already have happened?
> what's the solution here? speeding up the haskell compiler? if that were easy, would it not already have happened?
I suspect you’ve nailed the answer: it’s probably not easy, although it’s also possible that it just hasn’t ever had a lot of attention paid to it because it’s been generally fast enough for their user base?
Part of the issue is probably that Haskell build performance is perfectly fine for local development, even on rather large systems.
But in commercial production environments, CI pipelines tend to want to build everything from scratch every time, and that slows everything down. Rust has the same issue. Both languages, by default, compile all their dependencies from source, rather than obtaining precompiled artifacts from a repo the way some languages (like Java) do. And their compilers are slower than e.g. Go's. As the article mentions, various kinds of caching can help with that, but that's extra stuff you have to manage and deal with.
I'm not sure this is a bad thing, though. Haskell co-creator Simon Peyton-Jones coined the unofficial Haskell motto, "avoid success at all costs". I tend to agree with that. It would be difficult for Haskell to maintain its conceptual edge if it were a mainstream commercial language.
The thing I hate about rust is that compiling a small app immediately creates 100gb of junk, and that junk doesn't live in the responsible project's folder, and that junk doesn't get cleaned up by anything.
You can put the `cargo clean gc` command on a daily/weekly schedule then, or configure the `gc` option in your Cargo config. Or set `CARGO_UNSTABLE_GC=true` in your shell init. Or all of the above.
PLEASE_UNBREAK_ME options should default to on; people who actually work on rust code are quite capable of turning it off when they configure the rest of their environment.
I don't disagree. I can only assume it's defaulted to off due to many people working on big codebases and there constantly evicting cache would objectively slow them down. But, far from ideal for everyone else.
"proving code correct has been a research topic at some point."
It has been an area of active research for 40 years. But almost all the research returned the null result, meaning that the program proving didn't improve code quality (basically it didn't work). Yet somehow a group of programmers, usually fresh out of academia falls for program proving each generation. Strong types do really help but you need a good compiler which is sometimes lacking in the real work cough Scala cough. The problem with strong types and program proving is that the juice just isn't worth the squeeze meaning the extra time taken doesn't result in reduced debugging time or improved code quality. I don't think that changes with LLMs. It just exposes the flaws more quickly.
what i expect to change with LLMs is the benefit you get from automated testing. which is really what LLMs need. tools that tell you something is wrong may not speed up a human developer, but they will allow LLMs to make corrections by itself until the warnings go away. so while it may not be worth it to a human developer, it may well be worth it for an LLM.
The issue with formal proofs is that we might end up with correct code that does the wrong thing. I mean, something that the market doesn't care about. At the same time a buggy set of PHP scripts does what people care about and captures the market. Think about 20+ years ago and you find a lot of examples.
There's a reason why littering is the most common crime - it doesn't require intelligence or planning, and whatever you are holding can instantly become trash.
That's literally the opposite of reality; the research and adoption have consistently produced code of extremely high quality, at great cost.
The problem has always been:
- It's extremely labour-intensive, and even small changes to the code can require an enormous amount of new proof work.
- The skills required to formally verify software are very different from the skills required to write it, and the set of people capable of doing so is much smaller.
- Code has to be written with verification in mind, against a specification that expresses invariants that can actually be verified, and that is coherent enough that you can derive useful high-level properties of the system from it.
You needed two teams — verification and development. Verification would always be behind the development team, while also having to feed requirements and design changes back to the developers. Everything slowed to a crawl.
AI changes this. A coherent, verifiable, useful specification isn't easy to write — but it's far easier to write than the software itself, and AI can do most of that work: both drafting the spec and proving that it's consistent and that the high-level properties you actually care about follow from it.
More importantly, a high-level spec is far easier to read and reason about than the reams of code required to actually implement something. Which means:
- AI does the grunt work of writing and proving the spec; humans only have to carefully review that high-level artifact.
- AI writes code it must also prove conforms to the spec, so humans can be assured it's correct without babysitting the AI.
- Changes are driven top-down: evolve the spec first, then have the AI fix the implementation and re-prove conformance.
Our (very, very large) company is rapidly going all-in on formal verification across projects we never would have dreamed of verifying before; the velocity hit and the man-hour cost were only worth paying for truly critical infrastructure.
I’m sure in certain domains this makes sense. However, English is a poor language for doing reasoning in. More and more I’m relying on the code itself as the documentation. One of the superpowers of LLMs is reading code. and turning it into readable English. I don’t keep the English prose around. I delete it. In your example, I don’t see having two sets of artifacts. I see working with an LLM to generate a code base, which is the specification. You still need to have sets of requirements that list the invariants and other parameters. But the process becomes generating the code, and then having the LLM read the code to see if it meets the requirements and invariants.
I've been successfully practicing programming with proof of correctness since the 1970s. The confusion I see with most people failing at this is that they're trying to write code first and then prove it correct. The idea promoted by Dijkstra, Hoare, et al is to write the code and the proof together, with each of these aspects of the task supporting the other. The result is (in my experience) a better program developed as quickly (even when not counting on vastly decreased debugging time) as pure coding. Similarly with strong lexical typing: One does not try to create a typing model for a hunk of arbitrary code. Instead, one develops the type model along with the code, with each of those aspects supporting the other. Putting it all together: Coding, Typing (and other Constraining) and Proving are done together, with each of these activities supporting the others. It does take time to learn such a methodology. And it takes extra understanding to maintain types, constraints and proofs as the program evolves, i.e. "maintenance".
Recently had to touch a Python project at work. Just setting up the editor needed me to use 2-3 tools out of: pyright, basedpyright, ruff, ty, mypy, and possibly other tools I'm forgetting that kind of do the same thing but throw errors in different parts of the codebase.
Also, for some reason Optional[T] became deprecated, just as the ecosystem finally embraced types ~3 years ago.
In fact, one my company's greenfield projects decided to use TypeScript instead of Python for the [surprisingly] more consistent tooling, and the fact that the big LLM providers all have official TypeScript SDKs anyway. Also, for agentic coding, LLMs don't seem noticeably worse at TypeScript than Python.
My experience can be summarized as:
- for some reason we need 2-3 static analysis tools just for typechecking
- no tool understands each other's comment directives
- each tool reports a different error in your codebase
- even big libraries (e.g. matplotlib) make half their functions return Any
- you'll be tempted to silence the "partially unknown type" warnings, and you'll have to do it for each tool that's running.
> Also, for some reason Optional[T] became deprecated, just as the ecosystem finally embraced types ~3 years ago.
Optional[T] is now T | None. Means exactly the same thing but doesn't require an import. Support for the older syntax presumably won't be removed for a long while regardless.
It is only pyright/basedpyright that flags Optional[T] as deprecated as far as I am aware. Optional isn't actually deprecated by Python or anyone else.
You can disable it in the pyright settings. In my opinion T | None is not a meaningful improvement and insisting on changing it everywhere causes a whole bunch of churn and needlessly makes code stop working on older Python versions.
I grew up on python and after working with Java I really came to appreciate types. However I do want to point out that big libraries like mpl pre-date most efforts for typing, so it is no wonder that they arent typed properly. A lot of these libraries are trying to improve this but it will just take some time.
Having been around for quite some time, and having used several dynamic languages, my pet peeve isn't types.
Rather I prefer not to be in the same spot I was in 1999 - 2001, with Tcl, and every now and then rewriting code into C, for the application to actually deliver within the performance deadlines.
Python is the only mainstream dynamic language where runtime support for dynamic compilation is such an hassle, where the alternatives do exist, yet are mostly ignored.
In my (really large) typescript project we have like 6 static analysis tools running[1]. Steps to get the project running: install nodejs, install package manager, install dependencies, run project.
The main difference is that in the JS ecosystem it is all installed at the project level, you don't need anything globablly installed besides the runtime and package manager (and even the package manager can be auto-installed as well if you set it up that way).
[1]: eslint, biome, prettier, scass linter, graphql-codegen, tsc, tanstack-router codegen. That I remember, might be more (although codegen might not be considered static analysis, it is needed for static analysis).
I'm not who you replied to, but we use Biome for 99% of the linting/formatting, and then an eslint plugin for a few specific i18n scenarios that Biome doesn't cover.
Yeah we had some corner case that still required eslint, although I don't remember what exactly. We also have prettier for a few file types not supported in biome.
Although I also install such tools (linter, import sorter, type checker) locally in my virtualenvs in Python projects. It is possible to do so, but maybe not as straight forward. I have to give the JS ecosystem that much: With the project-local approach, they have done one thing right.
UV, Ruff and Pyrefy and you're set. As someone who works with Python, Typescript and C/Zig quite a lot I don't disagree with you on Typescript, but I'm not sure why you'd pick Typescript over Python. Bun is kind of awesome, but it's also kind of unfinished, but if you go with the default Node I find that the setup for security compliance is next to impossible where Python can do most things with it's standard library, a pandas and pyarrow.
I personally prefer the fake typing in Python because it fits well with our defensive programming style with very low abstraction and little to no adherence to DRY. Since Python naturally force you to deal with runetime assertions rather than getting you to do compiletime checks that then don't actually offer any form of safety at runtime. Which is obviously not a very technical argument, but it just feels a lot cleaner.
uv still can't build all wheels and afaik they don't intend to do so. Furthermore they leave their users with 0 indication that the build fails because the wheel is unsupported by uv. If I were a beginnner or intermediate, I'd definitely given up after some attempts of fixing the buildsystem/code of the wheel.
I don't get how uv regularly gets recommended without any note about this.
Can you name some example and explain what happens, that the user is left without indication of failures? I am asking, because I have not had such issues yet, but maybe I just don't know I had them?
Can you say more? uv should always tell you if a wheel build fails, unless the build backend (which uv doesn’t control, unless you use uv’s own backend) decides to silently ignore a wheel build. This would be a bug in any given build backend IMO.
This is an unfortunate complexity in Python packaging: something like `uv build` can dispatch a wheel build for you, but the actual code that gets run as part of that build is often third-party build backend code that uv itself has little to no control over.
It happened almost 6 months ago, trying to build https://github.com/remsky/Kokoro-FastAPI with uv. From what I can still infer from the dependency list & my shell history, the problematic dep had been pyopenjtalk. After building & installing this dep with pip, I was able to continue to use uv.
We get UV to generate a requirements.txt and then use the Python and Pip which is available on the official Microsoft container images we use for Azure container apps once it hits production. I've never had any issues with the build system in development though.
> for some reason Optional[T] became deprecated, just as the ecosystem finally embraced types
Deprecated in favour of `T | None` exactly because of that embrace. It's cleaner, more consistent (you can `T | U` arbitrarily), and helps slim down the `from typing` imports.
Yes, changing a type checker tool is not done easily in larger projects, or on the fly, as they all have different edge cases, where they don't infer well enough, or are more lenient than another tool.
I have switched type checker recently in my own Python projects from zuban to ty. ty seems to work better, and is not a one man show/bus factor of 1, though I respect the work that has gone into zuban by its creator. But ty doesn't understand mypy configuration in pyproject.toml ...
I imagine switching a type checker in a bigger project and with more people involved to be a bit of a PITA, until everyone has adjusted their development environment/tooling. Best one can do is research beforehand which tool suits one best, test it, and then stick to it, unless it has unbearable failures.
> for some reason we need 2-3 static analysis tools just for typechecking
I don’t follow: you need one type checker, of which you have several options. It’s arguably not ideal to have more than one option, but you should never need to run more than one.
- no tool understands each other's comment directives
In general, all type checkers in Python support the `type: ignore` directive, since it’s standardized.
> each tool reports a different error in your codebase
This is a real problem, but I think you can avoid it (like most people do) by not mixing different tools that do the same thing together.
To my understanding, you’d have the same problem if you combined (e.g.) biome and eslint in a JavaScript codebase.
I have to use more than one Python type checker because there is not a single one that works. Not only different tools catch different issues. They also have different bugs, and different configuration requirements. Different teams have different preferences.
It's a nightmare. If Python taught me something about typing is that a language that doesn't have a clear, well defined definition of types in the reference implementation, will never get it fixed with external tooling.
I had a similar experience as you. We normally use Kotlin for everything, but last year we had to do a small project in Python. Setting up the tooling and choosing the tools is quite overwhelming, and the inconsistency between what the tools actually consider type errors is incredibly frustrating. I am actually happy that the project failed and that we don't have to work in that environment anymore.
I think Python is probably good for many things, including scripts and as a starter language, but I don't understand how anyone can stand writing large software systems in it.
Not my experience at all. There are a couple of linters to choose from, and uv is becoming the dominant packaging environment. In my experience it is much easier to write large maintainable projects in Python than in TypeScript. Python has more language features and certainly a much better ecosystem. Who knows where things will go. In the future language choice will be much less important. Languages are largely a human artifact.
I think the author largely agrees with you re: type systems and LLMs. He's pretty explicit that Haskell should be very well positioned to be a power language for LLM-assisted programming, but that the Haskell ecosystem presents the bottlenecks that make it harder.
I don't personally use Haskell for anything, but I use Lean and occasionally some other languages with expressive type systems, and like you I've found it to be a pretty great experience for working with LLMs. But I've also experienced what the author is talking about, with languages that sit at different points on the type system spectrum, regarding a languages ecosystem/infra layer becoming a bottleneck. I don't think it's ultimately about the type system but the broader ergonomics of the language/ecosystem.
So I think his criticism is less than expressive type systems are a pre-LLM concept, and more that Haskell has an individually bad "agentic coding story".
Yes. To add more nuance to what I'm saying the trajectory of the industry and society is heading towards this.
You company may not be like this now, but the future is where this is all converging. I can tell you at my company as of now, if you're not using AI it is essentially career suicide.
AI makes people produce consistently worse results.
I will not under any circumstances use AI, not least because it cannot possibly solve a problem I have.
At work, I've already had to "fix" some incredibly faulty projects caused by well-intentioned people thinking they can use AI to write code. I started with `rm -rf *` and moved on from there.
That's you and your anecdotal experience against raw data from the entire world.
I don't get how people like you give their one off experiences and think that validates their world view when the world is changing right before your fucking eyes. To each their own and agree to disagree because honestly your anecdotal experience is right in front of your own eyes too. I just don't get how you can't reconcile that with the rest of reality of basically EVERYONE using AI all around you. How about some nuance like, sometimes AI screws up, but often it does better? That realistic viewpoint may not fit the world view you wish existed and so you choose the most one sided lack of naunce world view you can possibly make.
Do whatever you want. But somewhere in the back of your mind, you know you're just being stubborn.
It is a bit surprising, I'd have guessed the same. Although in hindsight I could believe that type systems aren't particularly strong as an anti-bug layer. They help. They're a big boon for coordinating large numbers of mid- and low- skill programmers though because it forces them to go further in documenting their function signatures and makes it much more obvious where the problems are when refactoring spaghetti code because things break loudly.
Refactoring spaghetti has become easier in the LLM era because it can just read all the code, and there is now a skill floor on the programmers that kicks in somewhere relatively high. The benefits of type systems might have suffered because of that.
> I could believe that type systems aren't particularly strong as an anti-bug layer.
They're absolutely huge for this, but you have to write code to take advantage of the guarantees that the type system can offer.
As Yaron Minsky at Jane Street put it, "make illegal states unrepresentable". Stronger type systems make it possible to make more states unrepresentable. You end up with what amounts to static debugging - you debug your code at compile time.
Sure, it's still possible for runtime bugs to occur, but entire classes of bugs are eliminated, plus it becomes possible to have static assurances about program states about things that most language don't even try to express in the type system, like security.
At least in C and Go, you can get structs where it's easy to reason about the fields.
In Python, a "class" is simply a dict under the covers and (by default at least) you can add attributes to it after definition (as well as things like properties). So it's difficult to reason about what the fields are at any given time. And that's assuming people USE classes! I've seen code where all the state is in one giant ever-changing dictionary and you have to pull out a debugger just to figure out what's IN the thing! God help you if you mispell a key!
Maybe you work with better quality code than I do, but I find Go's type system a lot easier to reason about than Python's.
I don’t believe that “reasoning” is very useful in large code bases written by multiple developers. If you are trying to be axiomatic and prove to yourself that the code is correct, there are a thousand different ways the part of the code may not support your axioms in ways that are not apparent. It’s better to have a handful of invariants that are well communicated to the team and a lot of tests.
Yeah this is what I meant, structs are obviously a huge step up in type-safety but the ways you can abuse or bypass the typesystem in C and Go means you still have to be super careful about the memory your code hold, receive or returns.
I think Go developers don't think this is much of a problem because most of them are working on microservices where data is not kept around for a long time (usually a single request). And of course Go makes it a lot easier to handle data-access parallelism. So the benefits of a powerful type system are somewhat diminished in this scenario.
I think it is one of the reasons why Go devs don't see much benefit in Rust, the borrow checker is just not that useful in the context of isolated requests.
The best thing React ever did to the JS ecosystem was hammer in on objects/maps/dicts being immutable. In modern JS it highly unnusual to see code modifying objects (except right after creating them) even though the language itself has no convenient means of representing that.
Right, they run tests too. A compiler is like a quick test before tests. How are you going to cut out that check and let the LLM "write it faster" is beyond me. The compiler catches errors across codebases that today's LLM can't economically or reliably put into context to perform similar checks. They're totally different tools, today.
Also, you can just compile less frequently.
But hey, if LLMs are what drove this person from Haskell to Lisp then all the power to them!
Even Emacs Lisp has had a byte code compiler since forever, and since Emacs 28 it compiles to native machine code via libgccjit.
Toy lisp interpreters are easy and fun and everywhere, often written in lisp itself, but the same books that teach them go on to teach compilers. SICP chapter 4 is the metacircular evaluator, chapter 5 is the compiler. That ordering is the point: interpretation is the pedagogy, compilation is the destination. Lisp was designed to be easy and efficient to compile (so was Self, and that tech lineage runs straight into HotSpot and V8). The archaeology cuts the other way too: the only famous pure interpreter was LISP I on the 704, and within two years Hart and Levin's Lisp 1.5 compiler (1962) became the first self-hosting compiler in any language, written in Lisp, compiling itself. Lisp practically invented compilation as we know it. And today SBCL doesn't even interpret by default: a form typed at the REPL is native code before it runs. Clojure compiles every form to JVM bytecode.
So antonvs, I'd genuinely like to know which famous implementations you mean, because the honest counterexamples aren't the famous Lisps at all. They're the embeddable extension languages, and I can speak to those from direct experience, not just archaeology.
A digression about Elk, since hardly anybody remembers it and Oliver Laumann deserves the recognition. Elk, the Extension Language Kit, was Laumann's Scheme out of TU Berlin, started in 1987 to be the extension language of the ISOTEXT document editor, released to the world in 1989. Its stated goal was "to end the recent proliferation of mutually incompatible Lisp-like extension languages": instead of inventing yet another one, you linked the Scheme interpreter into your C or C++ application and taught it your app's types and primitives. Emacs-style extensibility as a kit, years before Guile existed. And it was a pure interpreter on purpose. Laumann said it plainly in the USENIX paper: performance was "uninspiring (no compiler is available)" and it didn't matter, because any bottleneck got recoded as a C primitive. The compiled half of an Elk system was the application itself. It shipped that way in real products, including the TELES.VISION video conferencing system, which used Elk continuations as its multithreading mechanism. Elk's embedding design went on to inspire other interpreter authors, including Matz for Ruby.
I hacked on Elk back then and wrote the SPARC port, credited in the Elk 1.2 release notes, and it taught me why "just an interpreter" is never just an interpreter. Elk implemented full call/cc the brute force way: capture the registers setjmp-style, then copy the entire live C runtime stack aside onto the heap, Scheme frames, Xt frames, qsort frames and all, and copy it back whenever the continuation is applied. That works on machines where the stack actually lives in memory. On SPARC it does not: the register windows keep the most recent frames cached in the register file, invisible to any memcpy of the stack. So before copying you trap into the kernel to force every window to spill to RAM (ta ST_FLUSH_WINDOWS), grab the stack while it's momentarily telling the truth, and pray it all lines up with the calling conventions when you copy it back so returns don't sail off into the weeds. Frightening! As hairy as anything inside a code generator. The "simple interpreter" strategy relocates the machine-level work, it doesn't remove it: stack layout, register windows, calling conventions, incremental linking against the running executable so compiled C extensions could be loaded into the interpreter, unexec-style dumping of customized interpreters back out as executables. Compiler and linker work in everything but name.
And look what happened to every extension language that survived: Tcl grew a bytecode compiler in 8.0. Emacs Lisp went from bytecode to native code. Guile, after fifteen years as an interpreter, got a compiler in 2.0 and a JI...
You provoked me by being right, and I just wanted to say that I strongly agree, but it's "nuanced"! ;)
I didn't get a chance to post about my memories of Tom in the hn discussion of his death, because I hadn't really processed it in time to comment, so I'm writing them up now and will publish more later.
> andersmurphy and jwr have it exactly right about the REPL. Common Lisp and Smalltalk systems compile one function at a time, to native code, inside the running program, in milliseconds. Avi's headline brag, fixing a bug before the customer hangs up, was routine practice in Lisp shops in the 1980s. NASA once did it to a Lisp system running on a spacecraft a hundred million miles away: the Remote Agent on Deep Space 1 deadlocked in flight, and they diagnosed and fixed it through a REPL running on the spacecraft itself.
Ok, tell me how you're an "avid digital archaeologist" and you're not aware of what I'm about to tell you.
The original Lisp as designed by John McCarthy was interpreted. That became Lisp 1.5, which ran on several mainframes of the time.
Programs were represented directly as S-expressions and evaluated recursively. This is one of the defining features of Lisp - the metacircular evaluator! Almost any Lisp text which covers the implementation of the language will describe this. It's emblematic of what Lisp is!
Interlisp was interpreted up until its final versions, which added some JIT capabilities.
The PC versions of Lisp, like muLisp and XLisp, were interpreted. muLisp introduced compilation in later versions.
If you count Scheme as a Lisp, there are probably more Scheme interpreters than there are compilers.
The reason that several Lisp implementations added compilation in later versions is because the hardware limitations of the time meant interpreters had a substantial handicap against compilers. But compilation was never a fundamental feature of Lisp.
"When the compiler is called upon to compile a function, it looks for an EXPR or
FEXPR on the property list of the function name. The compiler then translates this S-expression into an S-expression that represents a subroutine in the LISP Assembly Language (LAP). LAP then proceeds to assemble this program into binary program space. Thus an EXPR, or an FEXPR, has been changed to a SUBR or an FSUBR, respectively.
Experience has shown that compiled programs run anywhere from 10 to 100 times
as fast as interpreted programs, the time depending upon the nature of the program.
Compiled programs are also more economical with memory than their corresponding
S-expressions, taking only from 50 per cent to 80 per cent a s much space.'
The major part of the compiler is a translator or function from the S-expression
function notation into the assembly language, LAP. The only reasons why the compiler is regarded as a pseudo-function are that it calls LAP, and it removes EXPRts and FEXPR1s when it has finished compiling."
The fun part is that the argument holds just as true for humans that write code - we also run tests to verify our work!
Which is basically what people liking dynamic languages have said all the time - types is only good as long as the overhead they bring doesn't cost more.
I general I agree with you. I think expressive type systems are superior, and they are even better in the LLM era.
I would quibble though that Python's is actually pretty good at this point, and, despite what the below poster is saying, straight-forward to set up and use. I am still perplexed that the author chose Python over Rust or Scala or TypeScript though, especially given they presumably want to migrate a Haskell codebase.
I'm perplexed by that too. We are migrating from Python to Rust simply because Rust is more suited towards unattended agentic loops, and we want to move in that direction. The results you get from a harness/agent/LLM with Rust are simply better than Python because the agent gets much better feedback from the compiler when it makes dumb mistakes. Python doesn't have anything even close to something like SQLx, which is a natural fit in Rust because of how Rust macros work.
> Python doesn't have anything even close to something like SQLx, which is a natural fit in Rust because of how Rust macros work.
I'd be interested in hearing/discussing more about this. I was very surprised, when I embarked on my side project, that Rust's options for SQL ORMs all seem so weird.
I think what you are referring to is the derivation of FromRow and stuff with SQLx, right?
I don't paticularly want to use an ORM. What SQLx does is a bit different - it statically analyses the SQL in your program against the database and ensures the SQL is valid.
This means you have rock solid assurance your program won't have database syntax errors at runtime. Basically it's just one less thing to worry about, and it's ideally suited for LLM-generated code.
I remember that being one of its "pros" when I was going over options with the LLM, but now, I just have a bunch of raw SQL strings in my codebase and sqlX's main use in that project, off the top of my head, is just instantiating Rust objects from the raw results with `FromRow` (it's probably doing more than I realize; I am not as connected with the code as I want to be, using LLMs to move fast to launch a couple features before revisiting a lot of the mess).
You’d be surprised. It works quite well without the static guard rails. But the static guard rails do improve things but not in some extremely obvious way.
> "At Scarf, we started doing all new API work in Python."
Start the countdown timer for how long it takes them to discover that was a mistake.
Nothing to do with Haskell, but good grief, LLMs do not in any way, shape or form save you from the deep, unfixable problems with Python.
At the very least you need all the static checking machinery like Ruff, Pyright, and hefty unit tests that take the place of typechecking if you don't want obvious failures to only show up in production.
I had this recently with an ML training pipeline, where Python is essentially forced on us. A dynamic error occurred after 17 hours of training - something that a real type system could have easily caught.
The solution that the LLM came up to prevent this in future was a complicated Enum-based system that just made me wish I could use a real programming language.
What would be your goto for the ML training pipeline?
I have the impression that Python basically wins by default in those spaces due to the lack of many good libraries in other languages (except for, like, C++).
But curious if this is just a very outdated view of the world
Yeah Python seems like a bad choice. LLMs seem to write low quality Python compared to Rust, presumably because there is a lot more low quality Python in their training sets than there is for Rust.
Unfixable errors? Why unfixable? Python is Turing complete. I can see difficult to fix, but not unfixable. LLMs lower the bar to refactoring code mistakes.
I have heard "the poor type safety" argument from writers of strongly typed languages for many many years. Having written js and python for a large amount of my carrier I can count on one hand the number of times I've found a bug that was due to a type issue. With LLMs it has been the same pattern. They don't seem to produce issues with types.
I've never done anything "serious" with haskell, just small personal projects. Mostly this is because I've found the ecosystem to be a pain - when I was trying stack stack was the thing to use but from what I can tell ghcup+cabal now work better.
If you push through that you end up with code written in a language people have used for formal proof (seL4 model is Haskell) and deployment wise a binary that +/- libraries you depend on ought to be reasonably portable.
I'm very surprised anyone would want to go the other way. Same ecosystem pain, plus you need to start shipping interpreters or containers, plus the language just doesn't really compare.
I know several people who are serious Haskell users despite, like you, using it just for small personal projects. All of them are quite some way down the autism spectrum and will casually toss around Haskell concepts that require about 30 minutes of googling by anyone else present in the conversation to try to understand. Get two or more of them talking to each other and everyone else present is more or less excluded from the conversation... and this is something they're doing just for fun, not because they're paid to do it.
It could just be a coincidence of statistics, but it does cover every Haskell user I know (needless to say, these people are much smarter than I am).
It makes sense, Haskell is basically just python from the code perspective. If it's faster to generate code than to compile it you might as well just keep generating til it works for your specific task.
My experience has been that the more type/safety checks the more chances there are of the AI getting stuck into a stupid loop
Because a lot of times it's missing the way of making the needed steps for the conversion (or it's just not obvious)
Sometimes it needs some nudging
Also this comment is a bit generic, it can also apply to cases where it's not an obvious "type check" but a redundancy that needs to exist but the AI can't get around
The only complaint against Haskell was about long compilation times.
I agree that short compilation times are very desirable, but I do not see why Python must be the solution for that.
I do not know whether Haskell can be compiled quickly, but from my experience, I am very certain that short compilation times are easily achievable for languages with good static type checking, especially with compilers that have different options that allow choosing between fast compilation and heavily optimized compilation.
An optimized compilation may require a much longer time than a fast compilation, but that has no relationship with the programming language used in the source text, but only with the intermediate representation used by the compiler and the target CPU ISA.
When you try to optimize a project by generating many variants with a LLM, I doubt that all those variants will be generated from scratch, completely independently, even if only for the reason that when using a commercial LLM the cost of a completely new variant will be much higher, by requiring many more tokens, so whenever possible it is preferable to generate other variants by just patching previous variants.
Whenever a variant is generated by editing a previous variant, incremental compilation can be used, which should be pretty much instant on modern computers.
> For many people, just having the function signatures is documentation enough.
You make a fair point about a documentation gap. The first step is always defining the problem. Wanting to add lots of realistic examples sounds like a wish for more tutorial or beginner-friendly content. Do you see the problem differently?
On the other hand, a given pure function type only has only so many possible implementations — why tools such as djinn and MagicHaskeller exist or why Hoogle is actually useful, unlike the horror of searching for every `void (*)(const char *)` in C.
From that perspective, Haskell docs tend to be more expert-friendly — perhaps a rationalization, granted — which seems ideally suited for an in-IDE model to help bridge between developer intent and typechecked code. However, this comes at the expense of putting in the reps to rewire the developer’s brain to think in functional terms and the resulting mind opening and horizon expansion to think new thoughts she wasn’t capable of even considering. In these days of LLMs, fretting over that particular opportunity cost may be thinking nostalgically about the loss of craftsmanship in fine, well-balanced buggy whips.
In the limit now, will all programming be strictly literate?
> I can't imagine using a language without a good type system
have you tried?
i use elixir and carefully watch the agents and its very seldom making typing mistakes (elixir is in-between, it's typed but only as a checker).
ultimately typing doesn't help as much because it's nonlocal information. if the system can locally infer what the shape of functions is, it's way better.
- I do not watch agents, at all. Rust and Typescript. When I use Typescript only I have some guidelines so that we build the stack to be as strict and type driven as possible.
Similar for me, I don’t want to babysit agents . I’m sticking with Python for the libraries (data science ecosystem), but I find that imposing ty and a few reasonable ruff rules (imposed with pre-commit so the agent can fix things automatically) improves agent-generated code a lot.
architecture matters more. at least for now, you should watch (not babysit) your agent, because it will slop up your architecture while you're not looking.
I'm watching because I'm particular about architecture. current project (company runs on it, its jira-lite + obsidian + benchling + a lab notebook, mendeley + a lims + a hardware kiosk that uploads scientific measurements directly to the notebook, everything running on cqrs and quill delta for operational transforms) is like ~100k loc and most new features are trivial (i just added a pcr wizard) because claude knows what particular patterns i use (and they are deliberately chosen to be more straightforward that conventional elixir/phoenix), but i do catch claude trying to do something chaotic from time to time. i suspect sometimes anthropic is routing me to a dumber model, because i can sense it happening, and if i keep going, it will make things really bad from an organizational standpoint, abd i have to roll back, but if i go to bed and pick it up next morning its fine.
next features are integrating a small transformer i built to codon optimize and probably "slack" to better communicate with the 2 ppl who work for me.
is your rust+typescript project at that level of complexity?
I'm not trying to be reductive but the article's a lot of words for "We're vibecoding our app now and the glorious (almost almighty) Haskell compiler is too slow for the agent to iterate it's mistakes until it gets it right."
This thought completely neglects the idea that Haskell probably need significantly less compiler runs because every run catches more errors and gives more information about them.
And that is not even considering how often the agent needs to run tests to get it right?
It seems pretty clear that they do only minimal live testing during the "open a ticket, implement something, deploy it in production, all while the customer is still on the call" cycles. So your second concern is probably not relevant in this particular setting.
Regarding the first, I think you're probably right, but then again, if there is a 15-minute base cost, it's hard to amortize that through fewer incremental runs of the compiler.
(Which isn't to say that I think they are doing the right thing.)
The number of compiler runs doesn't matter as much as the total elapsed time it takes to finish the task. In just about every test we ran, LLMs are faster at building in Python than Haskell.
Lisp/smalltalk programmers have been going on about this tradeoff for a long time. It mattered before LLMs too. Lisp/Clojure repl allowing you to compile tiny parts of your program inside your running program is incredible for your feedback/iteration loop.
Ironically, this is also what makes them shine with LLMs, the LLM has access to the running program and can modify it while it's running to get feedback instantly.
Complex type systems are cool. But, they are not free. I say this as someone who's first programming language was Haskell.
> I say this as someone who's first programming language was Haskell.
Amazing. How did that happen? Is it true that functional programming is only counterintuitive because almost everyone starts out with an imperative language?
> The number of compiler runs doesn't matter as much as the total elapsed time it takes to finish the task. In just about every test we ran, LLMs are faster at building in Python than Haskell.
How much faster? IOW, what's the difference (in minutes/seconds, not in percentages) between vibing Haskell and vibing Python?
Mixed with some complaints on how the community doesn't like vibe-coding, and that if you insist on not letting AI think for you, you will be left to die in the dust of the other competitors.
The amount of certainty random people have that LLMs have already revolutionized software development seems to be directly proportional to the media awareness of the AI companies finance unsustainability.
Granted LLMs aren't very good at the ML family of languages. Isn't vibecoding a lot safer with a typed language? Going from Haskell to Python is going from one end of the typing spectrum to the other.
I was puzzled by this article at first, but when you put it that way it makes more sense. The author also had a presentation awhile back about why Haskell is a terrible language for startups to adopt. I imagine that in the current venture environment vibe coding is being pushed heavily by investors since companies are effectively in an arms race where feature development cost is moving towards zero and investors don't care about individual companies because they have a portfolio of bets.
I don't think it has almost anything to do with vibe-coding. When I tried Haskell and OCaml some 3 years ago, they were infuriating in terms of tooling and just needless friction (don't ask, forgot, but it was mostly around package management and just entering a project and be able to run the program was a saga).
Iteration speed is not only to appease business people. As a long-time dev I prefer for my PL and its tooling to not stand in my way. I want to be enabled, not be left a scarred veteran with badges of honor like "Managed to install 5 dependencies and compile and run! And it only took me 3 hours!" or some such.
Compare to Golang, Rust, Elixir, and no small amount of others -- you add a dependency, run a command, twiddle your thumbs 30 seconds, done.
I want to solve problems. Not troubleshoot tooling that should have been prod-ready 10 years ago.
I've been using Haskell for 20 years. I've been running a company built on Haskell for 10. I have a very large legacy Haskell codebase. I've invested in several companies built on Haskell. Your anecdote is about your own lack of familiarity, nothing more.
Yes, companies can get into trouble with Haskell compile times, memory use, etc., but enforcing disciplined coding rules and advances in tooling solved the majority of the problems by 2016.
We use LLMs to assist in writing Haskell and have for several years. We keep track of compile times to catch regressions and on our largest code base an agent-driven change takes an average of 96 seconds (as of this month) to compile. Agents looking to do quick compile checks during code writing can run with flags that make the compilation run in less than 5 seconds. In all cases, running out test suites takes a lot longer than compile times.
The only time this isn't true is when you compile from cold start, as you would when using multiple work trees...which is the expected behavior when vibe coding features. So yes, I believe the issues they're running into have a lot to do with vibe coding. Check out all their integrations...Haskell's probably a poor match for what they do, but vibe coding integrations is probably what they need to do to get new customers.
IMO from the info given in the OP it seems to me that they never actually needed Haskell, not playing to its strengths anyway, especially if they felt Python was an adequate replacement -- which it absolutely is not if you value the compile-time finding of bugs, better known as "a strong static typing system". But that's a separate discussion vector.
To comment separately: lack of familiarity is true of course but do consider your bias -- familiarity should come quickly and easily. Long gone are my days where I wanted to wear badges of honor and suffer through difficult tooling, and I am willing to bet that's true for a lot of programmers.
We want to iterate and progress. You likely are used to Haskell's tooling quirks long time ago. Everyone else outside of the ecosystem does not have that advantage.
I am increasingly wondering if we are in a post-language world in terms of development. Why would I ask an agent to write a server in anything other than the most efficient language, although efficiency can take several forms: runtime, token usage during development, and wall clock dev time (affected by slow compile times for example).
My intuition is that type-safe languages with fast compilers are the best option. Maybe Go? I personally prefer Java just due to my experience running it in production, but am not sure there's many arguments for it over Go in a greenfield application. The other candidate would be Rust, but I worry about token efficiency and tool performance, I suspect it's not worth it for the runtime improvements.
All that being said, in this article switching to Python seems like a wild choice. Relatively poor performance, no compile time checking at all. Python's big selling point was developer ergonomics, which seems largely irrelevant now.
These are all just thoughts at the moment, I should try to find some evidence one way or another.
There is one simple thing you have to realize why Python is the optimal choice. You have so much training data. Python is the second most popular language on GitHub and is easy to read.
My thought reading this article was: Why write the system in any one language at all? And I don't just mean having some parts in one language and others in another language, I mean redundant implementations of the same parts. You can use an AI to rewrite parts of the system, and then throw away the old part... or you could just keep the old part.
That is: Have a Haskell base system. Have a Python "development" version on which you iterate at lightning speed. But also, in the background, moving at whatever pace it takes, have an agent running that imports all the Python development changes into the Haskell version. Have nightly builds of the Haskell version to reap its benefits (issues caught by the type system, more efficient native code).
And it doesn't have to be Python/Haskell of course. The "development" version could be a (hypothetical?) interpreted Haskell. I have no idea if ghci would be useful for this. Neither do I know if the 15-minute Haskell build time is spent in the frontend (so an interpreter would have to pay that cost too) or in code generation or linking (which the interpreter wouldn't need to care about). Anyway, these are things I would think about before I do what the OP did.
I felt similarly. I wonder why the author is so invested in Haskell specifically becoming an AI-pilled ecosystem and community when the choice of language rounds to not mattering?
Go is the perfect language for this new world. Its development loop is fast, it has types but not too much so, it is memory safe, it is easy to deploy and it runs efficiently enough for most line of business use.
Go was originally developed specifically for a world of interchangeable hard-working juniors, which is exactly what LLMs can scale up with only your budget as the constraint.
I'm wondering what advantage a hypothetical faster-compiling-Haskell would have in that world.
I'm curious about the choice of Python, rather than TypeScript.
I find Ruby a very beautiful language, and Rails is an excellent web framework, but I need typed functions, record types and sum types.
They help not just with correctness, but also as living documentation that lets me understand AI generated code. TypeScript provides discriminated union, but not exhaustive pattern-matching, and its syntax is a bit verbose, but since I'm no longer writing most of the code myself, I can live with it.
However I can't imagine using Python or any other dynamic language going forward. There is likely good reason for you to choose it, and I'm curious to know what that is.
I hadn't considered that, you are right. Though I would still prefer TS because the language is all about types; the entire ecosystem is well-typed and the type system is quite powerful (I do enjoy an occasional Omit and Pick). But it is a personal choice; as long as LLMs can generate well-typed Python, then for people who like the language it makes sense. However from the article, I got the sense that they went completely dynamic.
Yes, because the code you (or the LLM) writes is only part of the equation; if you use third party libraries then it becomes an ecosystem problem. I'm not currently using much Python but my understanding is that the community has not yet aligned on typing nearly as well as the TS community has.
This is quite insane to me. If I compare the output of LLMs for python vs statically typed languages it's really not a good choice to go the python route. It consistently produce relatively garbage code along actually good code. My experience has that the better static typing you have the better the code becomes.
LLMs have made me move away more from python rather than into it. I'm very surprised by this experiences of the author. The article is all over the place as well. Going basically all in on Python because it is apparently better than Haskell for LLM use and than agreeing with someone that says Rust is the best.
I've been a power user of LLMs for software development for a while now, and I've found two things to be true:
- The benefits of more "extreme" type systems are more accessible and valuable than ever. I have a fairly involved project built on Lean that I hope to open source this month, and it's been a joy to work in even for uses outside of mathematics.
- Readability, build time, infra complexity, and everything that affects your speed after finishing your implementation--these things now matter more than ever.
It's sort of a dual ergonomics problem, in some sense. And given that, the author's lament makes complete sense to me, especially:
"An AI-enabled Haskell ecosystem would ask different questions. How do we make Haskell easier for agents to use well? How do we get more high-quality Haskell examples into model training data? How can we scale reviews? How do we make library docs full of copy-pastable, realistic examples, not just beautiful types? How do we make project bootstrap fast? How do we make error messages more agent-friendly? How do we reduce cold build times? How do we make common industrial patterns obvious to a model that is trying to help?"
I'm not a Haskell developer and I hadn't heard of this company "Scarf" before.
As much as I respect this guy who tried to work and push an alternative ecosystem, it's hard for me to shake off the impression that, rather than due to Haskell compile time, he moved to python because it's easier to find developers for it and it's the de facto scripting language for LLMs.
No problem about that, of course. Running a company is hard enough, I think that passion and idealism for a language/platform/technology out of aesthetic appreciation can only go so far and after a certain age just making money and reaching your professional objectives count more.
Fast compile times is one of the most important qualities for developer productivity. It made Haskell a non-starter for many developers even before LLM driven development took off.
We at Cachix have also moved on from Haskell about two years ago and I'm sure someone is going to make a comeback with a language that takes the lessons from it but starts from skratch.
We need more general purpose Elm languages in the space.
"Hammers are now a very popular tool, and one can move quickly building exclusively with hammers, so we have decided to construct buildings strictly using nails, no more screws, bolts, or any other kind of fastener shall be used going forward."
You say this as if it were necessarily irrational.
Clothes became chain-stitched (and later lock-stitched) because machines could chain-stitch. If there were a super-efficient hammering machine, it could be better to figure out ways to use nails to replace screws in designs than to hold onto screws just for nostalgia's sake.
"If there were a super-efficient hammering machine, it could be better to figure out ways to use nails to replace screws in designs than to hold onto screws just for nostalgia's sake."
How about for the sake of the bridge continuing to stand? Or is that not a good enough reason for the accountants?
I strongly agree with the premise of this article, which is why I am surprised that the author moved away from Haskell to Python.
For some time now it’s felt clear (or at least extremely) compelling that agents need fast compile times in order to be effective, especially when you’re working in parallel. But the other thing that has felt just as obvious is that agents need strong type systems and narrow guardrails in order to constrain their outputs. These two things felt clear enough to me that, like the author, I wanted to choose a language ecosystem that maximized them. There _are_ languages that both have expressive type systems _and_ fast compile times. I wonder if the author investigated any of them, before deciding that no compilation time at all was acceptable.
In my case I landed in OCaml. I think there are other options in the space—Go if you want less typing but faster compiles; Rust if you want more types but slower compiles. My mostly vibes-based evaluation landed on OCaml, and I’ve been pretty happy with the results.
>agents need strong type systems and narrow guardrails
I read the second paragraph as saying close to the opposite of that, particularly "the model can often avoid the mistake before the compiler ever sees the code. And as the models get better, the relative value of catching every possible issue at compile time changes."
In other words, LLMs are much less likely to make dumb fat-finger mistakes than humans, ergo the value of type checking has fallen.
Everything in the prior sentence is, obviously, highly debatable.
My experience with Claude and Ruby, Python, Javascript is similar: it's pretty good at finding the array of strings that was passed to a method instead of an array of integers. Think about record ids coming from a JSON API call. Or the single value instead of an array. I don't remember which Python XML parser is fond of returning one or the other according to the cardinality of the sub elements. Anyway, not only it writes the code to handle those cases but it traces the code and it finds the bugs. So type checking at coding time and who cares about writing the type annotations. They would be probably good to speed up the code at runtime but none of my customers use them and none of them is concerned about the current response time of their systems.
- None of us are experts in Rust, and we're all solid at Python.
- Rust felt like an under-correction for what we wanted (get all friction in front of the LLM out of the way).
- Our high-performance stuff is not being migrated at this time (Scarf Gateway), so we're just talking about basic CRUD backends here. Basically any language will work.
Python is preferred because Python programmers are cheaper than other languages. Not because of any sort of technical advantages. Its literally the worse performing programming language in popular use. And it uses invisible characters in its syntax. Truly, it is the VHS of our industry.
True that an interpreted language has a leg up on any compiled language in the arena of compile time, but worth noting that one of Go's primary design goals was improving compile times of massive code bases. Google was drowning under the weight of compiling huge C++ codebases and Go was the response to that (among other things).
Go compiles things at package-level granularity. You only need to recompile your reverse dependencies on making changes. Also there's build caching available out-of-the-box, as well as some support for test caching.
You might not like Microsoft but they did a video on why they re-wrote the last version of the Typescript compiler in Go. Basically, because of LLMs. It's worth viewing even if you don't decide to go with Go.
Using a frontier model you can combine it writing high-quality Rust and educate you in the process. You guys were too scared, needlessly.
But as others said: if Haskell was not giving you too much in terms of excellent typing system then maybe going for a PL with a much faster compiler like Golang would have been best for you.
I also went through quite a process to select a language to work with LLMs[1] before settling on OCaml.
I am not unhappy with the choice and find it works quite well, with relevant skills loaded, but I am always interested in others’ experience and understanding what they’ve discovered works well.
I was very happy with GLM 5.1 and Kimi 2.7.
I did see an improvement in GLM 5.2. I found it to be more realistic about what was and wasn't good architectural choices.
I have a pretty elaborate harness setup, I use my own spec DSL to plan and execute code. So what works for me may not totally apply to others.
I haven't used the frontier models in a few months, so I can't speak at all to this latest batch. When I switched over from frontier I found that what I lost in training data, I more than made up for with Pi harness.
Your process seems more thorough than mine. Mine was literally "I like camels". Switching over to OCaml I found wonderful - I had not worked with FP before and I found it delightful, it kind of cured my programming burnout a bit. I hand built a CRUD app in OCaml/Dream but still feel like a rank beginner, especially when it comes to actually writing code, lol. But I'm getting much better at reading it.
I think the typing and structure of OCaml is a great fit for LLMs, but also the proximity to other things like DSLs, formal proofs, and algebraic structures has tons of untapped potential.
I'm having an absolute blast with it. I like programming again.
One more thing - just seeing your affinity for Julia. I built a fully interpreted Lisp in OCaml which you might find interesting. I added my email to my profile here, feel free to reach out.
LLM are string happy instead of using ADT with ocaml.
That was my main pain point with it. Otherwise fairly happy, I would feel C#/.net core would be the best overall in term of language/platform.
Personally been experimenting in Lean 4. LLMs understand it, can be given simple rules to improve it. Typing is strong, proofs are solid, and it compiles quickly.
On the contrary, for a small rust project, I had to clean out 180gb of cargo nonsense from the last ~3 days worth of compiles on a single, narrowly focused topic branch.
The library situation might be funky, but I'm also learning Lean 4 by hand. The tooling & lsp integration is lovely.
Jane Street has unlimited engineers and money to do this.
At least from the sidelines, it looks like they've built/forked most parts of the OCaml ecosystem to make it work (OxCaml, Dune was built by then, custom profilers, custom networking hardware, etc).
Of course a lot of their work is open source and can be used, but I wouldn't assume that it would be worth the effort to do something similar with OCaml yourself. Maybe it is! LLMs go brrr and all that.
Yeah, I don't get it. There are so many real-world bottlenecks that it doesn't matter if you can edit the code 10x faster. It could even be beneficial to tactially slow down some parts of your SDLC.
For what it's worth, I've been using Haskell in production at Bitnomial, a financial exchange, and LLMs + Haskell is an extremely productive combo.
Since Opus 4.6, LLMs have been pretty clever at using fancy types with libraries like Servant and Beam. The expressiveness of the types, combined with feedback from the compiler, means that agents converge quickly to something that works. I don't think I've noticed agents having to run the compiler so often that compilation speed is an issue.
I'd be very curious to hear your take if you gave another language a proper try for comparison with the same tools. I think you'll be as surprised as we were.
Hey, we have a 10 year old Haskell/Python/C++/TypeScript/Nix codebase, and use all of them regularly.
Haskell compiles slowly, but we have not found that to significantly inhibit AI-supported dev so far. We use ghci with `-fobject-code`, and our largest leaf module (10k lines of webserver request handlers) takes 8 seconds to `:reload`. To run stuff in it, the agent pipes `:reload`, or other invocations, into `ghci`.
Working on parts parts that are early in the module chain, such as our Prelude, makes the whole-project `:reload` take 50 seconds. Much of that could be avoided if we didn't suffer from the TH recompilation problem (https://gist.github.com/nh2/14e653bcbdc7f40042da3755539e554a). Originally I made a small GHC patch to hack that out (what this conservative recompilation protects from cannot happen in our project), which made the reload much faster, but the logic was changed in recent GHC so that doesn't work anymore.
In C++, we have some individual files (and thus compilation units) that take 45 seconds to compile.
Python is of course the fastest to (build+run). However, Python also has some problems with repeated runs: Once you have many imports, just starting the program (e.g. `--help`) takes ~2 seconds resolving imports. Do `import pytorch`, add another 2 seconds. For repeated runs, this can be a pain; Haskell and C++ are much faster for that, so they win when you don't have to change the code but repeatedly use already-compiled tools in a different way.
In all 3 languages, getting things to typecheck is very fast, because the agent directly reads the vscode LSP typechecking errors which are < 1s feedback. Claude Opus 4.6-4.8 understand all 3 languages very well.
I agree that GHC devs should focus most of their efforts on compile speed, and real-world pain solving. Some parts of that are indeed being done (e.g. newest GHC can write bytecode to disk to make the above workflow much faster, and codegen is by far the slowest part of compilation). But I think the focus should be even more on that. I consider most important and unsolved:
* solving Generics being slow to compile, especially for types with many constructors
* solving deriving classes being slow to compile
* solving TemplateHaskell causing too much recompilation
* doing staged compilation,
so that the next module can typecheck as soon as its imports are typechecked,
as opposed to waiting that codegen is done;
this unlocks a large amount of parallel work availability
All of these have open GHC tickets that I think should be the highest focus.
Other real-world production things are being solved quite nicely currently:
* The new Haskell debugger
* Much better stacktraces
* Much better runtime introspection to debug runtime hangs etc
I get your general point that if iteration speed (as in change code + run, 100s of times a day) is your highest value, Python does quite well. We intentionally chose Python for the part of the codebase that's relatively simple data importing but from 50 different sources, count growing adding new sources all the time, and need just quickly iterate on each until we got it.
But I find it would not be a great language for the other parts. Its concurrency story is bad, its interpreter is very slow and immediately disqualifies Python when you need to do e.g. a line of code for every 1000 Bytes (say for streaming small data chunks), typing is very useful but bolted-on and not always correct.
Things where Haskell remains best-in-class is anything nontrivial-webservers, I/O, program correctness, the main "general purpose" programming, refactoring and long-term maintainability.
I like this article, but I would take some issue with the concept of the percentage of time taken up being a major issue.
If you go from taking 2 days to write some code and 20 minutes to type check (which does seem long, don't get me wrong, but still) to 10 minutes to prompt some code and 20 minutes to type check, that percentage increase to me isn't enough to justify switching.
You're still almost 2 days ahead, and converting those 20 minutes to 20 seconds are not going to make you ship a feature appreciably faster. But those types stand strong and I don't believe they can yet be replaced by an LLM believing they're correct.
Having said that, I also think that Haskell should massively speed things up. Having strong types if nothing else should surely produce some amazing type-checking speed wins.
Languages designers will have to make a choice whether to continue to design for humans or for big slop machines. The design goals are not compatible. This is obvious. I don't understand how anyone can miss such an obvious point.
Another obvious point is that an industry that runs on code slop will stagnate in terms of language an human tooling design.
This is a good post. AI has changed the programming language trade-offs and, as someone running a company that uses both Haskell and Python, I hope that Haskell can adapt to this new era.
I would like to add one additional observation, since we have been using both Haskell and Python in production for a long time:
Haskell excels at platform work, while Python excels at product work.
Our infrastructure teams work in Haskell (and also Rust nowadays), while our product teams work in Python. This gives us the best of both worlds (in my opinion): fast and rock-solid infrastructure on the platform side, and fast development speed and quick iteration cycles on the product side.
This setup has worked well for years for us, but it remains to be seen how and if this is going to change as well in the new AI era.
Very surprised about this decision. I use strict languages much more with LLMs and they improve quality a lot. Working on a big python codebase is very painful with an LLM.
I'm internally dogfooding my take on the stack that makes all these problems go away.
Everything sort of exists, but it's this heinous zero documentation, high pain tolerance thing: buck2 and RBE with NativeLink and hooking that up to action runners and it needs to all work in a container or on nix or in a deb and on MacOS, you hand roll the auth and the certs and where do your compilers come from, can it do NVIDIA, can it do mobile.
Problem is switching off Haskell doesn't help for long: the agents proliferate and you're back where you started with more bugs. So I've been sucking it up and getting all this shit one click and it works. This is good enough for my use, and if the Scarf folks want a solution and are willing to work with a garage band startup, I'd be open to doing a closed alpha. I have a buck2 where you write the rules in Haskell (if you even need to change the prelude, it ships with a WASM that isn't coupled to fbcode), and the Nix cache/substitutor is backed by NativeLink so it scales to anything and it speaks all the protocols correctly and with a verified supply chain.
I'm not even really sure this will become a product, I just need it, but I sort of suspect others will need it too. If there's interest I'll put up a landing page with an email sign up thing.
We're a Haskell shop (and have been for over 10 years now) and are finding agentic development with Haskell to work pretty damn well.
Cold compile times in Haskell are painful indeed. Our development practices don't really cause us to do that much - even with agents.
It's unclear to me if the development practices at Scarf that cause them to hit this pain often are worth it if it means giving up Haskell because the compile times are too bad. Maybe they are, but I don't think so.
Speaking as someone who has tried Haskell but hasn’t ever really gotten into it, but spends all day with a C++ codebase that also has long cold-compile times… I think the author of the post said they’re using git worktrees to be able to have multiple agents working on different things at the same time without stepping on each others’ toes. I’ve started experimenting with that myself and it’s great in a lot of ways but by having a separate source tree, it does trigger the cold compile problem. Two agents compiling the code in separate worktrees are working on entirely separate builds (great!) but that means there’s no shared compilation cache between them (not great). Is that something that’s tripped you? Have you found a good solution?
Just spit balling: could you have the agent pre warm the cache as part of your workflow? Like at the start of working on the code have the agent run a compile in the background. That way when the agent is ready to "really" compile there is a warm cache
Bazel maintains a system-wide cache that fixes this. At $current_employer a truly cold build (rm - rf the system cache) is 10 minutes or so. In a new worktree with a warm system cache it's under 60 seconds (still needs to build the worktree specific analysis cache). A fully warmed build is 10 seconds.
> We're a Haskell shop (and have been for over 10 years now) and are finding agentic development with Haskell to work pretty damn well.
It not that it didn't work at all, so much as that it worked much better in other languages when we compared side-to-side. It's possible we had dev practices that could have been modified further to suit Haskell better (we had been doing that for years already), but increasingly I want the toolchain to adapt to us, not the other way around.
Even without considering vibecoding, compile time and compiler/lsp memory usage are my main concerns with Haskell. In fact, I'd say they're even more of an issue if you're going without LLM's since they affect human beings ;)
277 comments
[ 3.0 ms ] story [ 89.4 ms ] threadBut I do think what benefits LLMs is the speed and accuracy of feedback. Type systems cover the accuracy part, but haskell was killing them on speed. It seems like a strange choice to go so far the other way on accuracy when there's a lot of languages in between. But I'm not familiar with the project so not in a position to call it.
It's not also really about expressiveness IMO. I've found LLMs to be best with more constrained type systems: they are better at ocaml than they are at typescript.
But I'd look at people a bit oddly when they said: 'We didn't want to set up CI caching and compiled languages took 30 minutes per run so we changed our entire codebase to python'. Maybe it makes sense for them, and caching across dynamically spawned VM's is admittedly a harder problem which most build systems aren't great at, but still a very large jump
Or if staying on Linux, JVM snapshots.
Secondly, there are several ways how Java source code becomes machine code, depending on which JVM and JDK is being used, not taking into account the ART cousin.
When the potential set of behaviors you could write a program to have is infinite, but the actual behavior you want is singular, a programming language is more importantly defined by which ones it eliminates up front than which ones it lets you write (assuming it lets you write the one you want at all, but that's almost always going to be the case for most general purpose languages). Bugs are just false positives in this framing, where the program you wrote seems like the one you wanted, but there's some divergence between what you thought you were getting and what you actually got, and catching some of those up front is a huge part of why type systems are so useful.
so does this mean that the LLM writes code that is so good that the compiler does not find any more errors?
or is it due to the nature of haskell that makes it hard to write bad code to begin with?
or just that because the haskell compiler catches more errors there is less broken haskell code of the AI to train on?
and what does that mean for the switch to python? if the python compiler/interpreter doesn't catch as many errors do we even know that the code is good?
or is this more like the belief if the LLM cab generate good haskell code, surely it can also generate good python?
what's the solution here? speeding up the haskell compiler? if that were easy, would it not already have happened?
I suspect you’ve nailed the answer: it’s probably not easy, although it’s also possible that it just hasn’t ever had a lot of attention paid to it because it’s been generally fast enough for their user base?
But in commercial production environments, CI pipelines tend to want to build everything from scratch every time, and that slows everything down. Rust has the same issue. Both languages, by default, compile all their dependencies from source, rather than obtaining precompiled artifacts from a repo the way some languages (like Java) do. And their compilers are slower than e.g. Go's. As the article mentions, various kinds of caching can help with that, but that's extra stuff you have to manage and deal with.
I'm not sure this is a bad thing, though. Haskell co-creator Simon Peyton-Jones coined the unofficial Haskell motto, "avoid success at all costs". I tend to agree with that. It would be difficult for Haskell to maintain its conceptual edge if it were a mainstream commercial language.
You can also limit it with an env var. I have capped mine at 10GB.
You can put the `cargo clean gc` command on a daily/weekly schedule then, or configure the `gc` option in your Cargo config. Or set `CARGO_UNSTABLE_GC=true` in your shell init. Or all of the above.
It has been an area of active research for 40 years. But almost all the research returned the null result, meaning that the program proving didn't improve code quality (basically it didn't work). Yet somehow a group of programmers, usually fresh out of academia falls for program proving each generation. Strong types do really help but you need a good compiler which is sometimes lacking in the real work cough Scala cough. The problem with strong types and program proving is that the juice just isn't worth the squeeze meaning the extra time taken doesn't result in reduced debugging time or improved code quality. I don't think that changes with LLMs. It just exposes the flaws more quickly.
Zuckerberg became rich on php, which grinds my gears almost as much as the genocide thing.
In fairness, my peers at $corp used to ship while I was sad-Wojciech "no you can't create tech debt". The universe is cruel to people who care.
The problem has always been:
- It's extremely labour-intensive, and even small changes to the code can require an enormous amount of new proof work.
- The skills required to formally verify software are very different from the skills required to write it, and the set of people capable of doing so is much smaller.
- Code has to be written with verification in mind, against a specification that expresses invariants that can actually be verified, and that is coherent enough that you can derive useful high-level properties of the system from it.
You needed two teams — verification and development. Verification would always be behind the development team, while also having to feed requirements and design changes back to the developers. Everything slowed to a crawl.
AI changes this. A coherent, verifiable, useful specification isn't easy to write — but it's far easier to write than the software itself, and AI can do most of that work: both drafting the spec and proving that it's consistent and that the high-level properties you actually care about follow from it.
More importantly, a high-level spec is far easier to read and reason about than the reams of code required to actually implement something. Which means:
- AI does the grunt work of writing and proving the spec; humans only have to carefully review that high-level artifact.
- AI writes code it must also prove conforms to the spec, so humans can be assured it's correct without babysitting the AI.
- Changes are driven top-down: evolve the spec first, then have the AI fix the implementation and re-prove conformance.
Our (very, very large) company is rapidly going all-in on formal verification across projects we never would have dreamed of verifying before; the velocity hit and the man-hour cost were only worth paying for truly critical infrastructure.
Also, for some reason Optional[T] became deprecated, just as the ecosystem finally embraced types ~3 years ago.
In fact, one my company's greenfield projects decided to use TypeScript instead of Python for the [surprisingly] more consistent tooling, and the fact that the big LLM providers all have official TypeScript SDKs anyway. Also, for agentic coding, LLMs don't seem noticeably worse at TypeScript than Python.
My experience can be summarized as:
- for some reason we need 2-3 static analysis tools just for typechecking
- no tool understands each other's comment directives
- each tool reports a different error in your codebase
- even big libraries (e.g. matplotlib) make half their functions return Any
- you'll be tempted to silence the "partially unknown type" warnings, and you'll have to do it for each tool that's running.
Optional[T] is now T | None. Means exactly the same thing but doesn't require an import. Support for the older syntax presumably won't be removed for a long while regardless.
You can disable it in the pyright settings. In my opinion T | None is not a meaningful improvement and insisting on changing it everywhere causes a whole bunch of churn and needlessly makes code stop working on older Python versions.
Rather I prefer not to be in the same spot I was in 1999 - 2001, with Tcl, and every now and then rewriting code into C, for the application to actually deliver within the performance deadlines.
Python is the only mainstream dynamic language where runtime support for dynamic compilation is such an hassle, where the alternatives do exist, yet are mostly ignored.
The main difference is that in the JS ecosystem it is all installed at the project level, you don't need anything globablly installed besides the runtime and package manager (and even the package manager can be auto-installed as well if you set it up that way).
[1]: eslint, biome, prettier, scass linter, graphql-codegen, tsc, tanstack-router codegen. That I remember, might be more (although codegen might not be considered static analysis, it is needed for static analysis).
So same as JS then.
I personally prefer the fake typing in Python because it fits well with our defensive programming style with very low abstraction and little to no adherence to DRY. Since Python naturally force you to deal with runetime assertions rather than getting you to do compiletime checks that then don't actually offer any form of safety at runtime. Which is obviously not a very technical argument, but it just feels a lot cleaner.
I don't get how uv regularly gets recommended without any note about this.
Can you say more? uv should always tell you if a wheel build fails, unless the build backend (which uv doesn’t control, unless you use uv’s own backend) decides to silently ignore a wheel build. This would be a bug in any given build backend IMO.
This is an unfortunate complexity in Python packaging: something like `uv build` can dispatch a wheel build for you, but the actual code that gets run as part of that build is often third-party build backend code that uv itself has little to no control over.
Deprecated in favour of `T | None` exactly because of that embrace. It's cleaner, more consistent (you can `T | U` arbitrarily), and helps slim down the `from typing` imports.
I have switched type checker recently in my own Python projects from zuban to ty. ty seems to work better, and is not a one man show/bus factor of 1, though I respect the work that has gone into zuban by its creator. But ty doesn't understand mypy configuration in pyproject.toml ...
I imagine switching a type checker in a bigger project and with more people involved to be a bit of a PITA, until everyone has adjusted their development environment/tooling. Best one can do is research beforehand which tool suits one best, test it, and then stick to it, unless it has unbearable failures.
> for some reason we need 2-3 static analysis tools just for typechecking
I don’t follow: you need one type checker, of which you have several options. It’s arguably not ideal to have more than one option, but you should never need to run more than one.
- no tool understands each other's comment directives
In general, all type checkers in Python support the `type: ignore` directive, since it’s standardized.
> each tool reports a different error in your codebase
This is a real problem, but I think you can avoid it (like most people do) by not mixing different tools that do the same thing together.
To my understanding, you’d have the same problem if you combined (e.g.) biome and eslint in a JavaScript codebase.
I have to use more than one Python type checker because there is not a single one that works. Not only different tools catch different issues. They also have different bugs, and different configuration requirements. Different teams have different preferences.
It's a nightmare. If Python taught me something about typing is that a language that doesn't have a clear, well defined definition of types in the reference implementation, will never get it fixed with external tooling.
It's a complete ergonomic travesty that Python doesn't have one.
I think Python is probably good for many things, including scripts and as a starter language, but I don't understand how anyone can stand writing large software systems in it.
I don't personally use Haskell for anything, but I use Lean and occasionally some other languages with expressive type systems, and like you I've found it to be a pretty great experience for working with LLMs. But I've also experienced what the author is talking about, with languages that sit at different points on the type system spectrum, regarding a languages ecosystem/infra layer becoming a bottleneck. I don't think it's ultimately about the type system but the broader ergonomics of the language/ecosystem.
So I think his criticism is less than expressive type systems are a pre-LLM concept, and more that Haskell has an individually bad "agentic coding story".
One approach would be to not use LLMs.
You company may not be like this now, but the future is where this is all converging. I can tell you at my company as of now, if you're not using AI it is essentially career suicide.
I will not under any circumstances use AI, not least because it cannot possibly solve a problem I have.
At work, I've already had to "fix" some incredibly faulty projects caused by well-intentioned people thinking they can use AI to write code. I started with `rm -rf *` and moved on from there.
I don't get how people like you give their one off experiences and think that validates their world view when the world is changing right before your fucking eyes. To each their own and agree to disagree because honestly your anecdotal experience is right in front of your own eyes too. I just don't get how you can't reconcile that with the rest of reality of basically EVERYONE using AI all around you. How about some nuance like, sometimes AI screws up, but often it does better? That realistic viewpoint may not fit the world view you wish existed and so you choose the most one sided lack of naunce world view you can possibly make.
Do whatever you want. But somewhere in the back of your mind, you know you're just being stubborn.
The type safety we gave up hasn’t been noticeable in any concrete way yet, especially considering our test coverage has never been better.
Refactoring spaghetti has become easier in the LLM era because it can just read all the code, and there is now a skill floor on the programmers that kicks in somewhere relatively high. The benefits of type systems might have suffered because of that.
They're absolutely huge for this, but you have to write code to take advantage of the guarantees that the type system can offer.
As Yaron Minsky at Jane Street put it, "make illegal states unrepresentable". Stronger type systems make it possible to make more states unrepresentable. You end up with what amounts to static debugging - you debug your code at compile time.
Sure, it's still possible for runtime bugs to occur, but entire classes of bugs are eliminated, plus it becomes possible to have static assurances about program states about things that most language don't even try to express in the type system, like security.
In Python, a "class" is simply a dict under the covers and (by default at least) you can add attributes to it after definition (as well as things like properties). So it's difficult to reason about what the fields are at any given time. And that's assuming people USE classes! I've seen code where all the state is in one giant ever-changing dictionary and you have to pull out a debugger just to figure out what's IN the thing! God help you if you mispell a key!
Maybe you work with better quality code than I do, but I find Go's type system a lot easier to reason about than Python's.
I think Go developers don't think this is much of a problem because most of them are working on microservices where data is not kept around for a long time (usually a single request). And of course Go makes it a lot easier to handle data-access parallelism. So the benefits of a powerful type system are somewhat diminished in this scenario.
I think it is one of the reasons why Go devs don't see much benefit in Rust, the borrow checker is just not that useful in the context of isolated requests.
Also, you can just compile less frequently.
But hey, if LLMs are what drove this person from Haskell to Lisp then all the power to them!
I didn't see Lisp mentioned in the article. They moved to Python. Which is certainly a choice.
Besides, “machine code generation” is not a fundamental part of Lisp. Some of the most famous implementations were pure interpreters.
Toy lisp interpreters are easy and fun and everywhere, often written in lisp itself, but the same books that teach them go on to teach compilers. SICP chapter 4 is the metacircular evaluator, chapter 5 is the compiler. That ordering is the point: interpretation is the pedagogy, compilation is the destination. Lisp was designed to be easy and efficient to compile (so was Self, and that tech lineage runs straight into HotSpot and V8). The archaeology cuts the other way too: the only famous pure interpreter was LISP I on the 704, and within two years Hart and Levin's Lisp 1.5 compiler (1962) became the first self-hosting compiler in any language, written in Lisp, compiling itself. Lisp practically invented compilation as we know it. And today SBCL doesn't even interpret by default: a form typed at the REPL is native code before it runs. Clojure compiles every form to JVM bytecode.
So antonvs, I'd genuinely like to know which famous implementations you mean, because the honest counterexamples aren't the famous Lisps at all. They're the embeddable extension languages, and I can speak to those from direct experience, not just archaeology.
A digression about Elk, since hardly anybody remembers it and Oliver Laumann deserves the recognition. Elk, the Extension Language Kit, was Laumann's Scheme out of TU Berlin, started in 1987 to be the extension language of the ISOTEXT document editor, released to the world in 1989. Its stated goal was "to end the recent proliferation of mutually incompatible Lisp-like extension languages": instead of inventing yet another one, you linked the Scheme interpreter into your C or C++ application and taught it your app's types and primitives. Emacs-style extensibility as a kit, years before Guile existed. And it was a pure interpreter on purpose. Laumann said it plainly in the USENIX paper: performance was "uninspiring (no compiler is available)" and it didn't matter, because any bottleneck got recoded as a C primitive. The compiled half of an Elk system was the application itself. It shipped that way in real products, including the TELES.VISION video conferencing system, which used Elk continuations as its multithreading mechanism. Elk's embedding design went on to inspire other interpreter authors, including Matz for Ruby.
I hacked on Elk back then and wrote the SPARC port, credited in the Elk 1.2 release notes, and it taught me why "just an interpreter" is never just an interpreter. Elk implemented full call/cc the brute force way: capture the registers setjmp-style, then copy the entire live C runtime stack aside onto the heap, Scheme frames, Xt frames, qsort frames and all, and copy it back whenever the continuation is applied. That works on machines where the stack actually lives in memory. On SPARC it does not: the register windows keep the most recent frames cached in the register file, invisible to any memcpy of the stack. So before copying you trap into the kernel to force every window to spill to RAM (ta ST_FLUSH_WINDOWS), grab the stack while it's momentarily telling the truth, and pray it all lines up with the calling conventions when you copy it back so returns don't sail off into the weeds. Frightening! As hairy as anything inside a code generator. The "simple interpreter" strategy relocates the machine-level work, it doesn't remove it: stack layout, register windows, calling conventions, incremental linking against the running executable so compiled C extensions could be loaded into the interpreter, unexec-style dumping of customized interpreters back out as executables. Compiler and linker work in everything but name.
And look what happened to every extension language that survived: Tcl grew a bytecode compiler in 8.0. Emacs Lisp went from bytecode to native code. Guile, after fifteen years as an interpreter, got a compiler in 2.0 and a JI...
I didn't get a chance to post about my memories of Tom in the hn discussion of his death, because I hadn't really processed it in time to comment, so I'm writing them up now and will publish more later.
WOW.
The original Lisp as designed by John McCarthy was interpreted. That became Lisp 1.5, which ran on several mainframes of the time.
Programs were represented directly as S-expressions and evaluated recursively. This is one of the defining features of Lisp - the metacircular evaluator! Almost any Lisp text which covers the implementation of the language will describe this. It's emblematic of what Lisp is!
Interlisp was interpreted up until its final versions, which added some JIT capabilities.
The PC versions of Lisp, like muLisp and XLisp, were interpreted. muLisp introduced compilation in later versions.
If you count Scheme as a Lisp, there are probably more Scheme interpreters than there are compilers.
The reason that several Lisp implementations added compilation in later versions is because the hardware limitations of the time meant interpreters had a substantial handicap against compilers. But compilation was never a fundamental feature of Lisp.
https://softwarepreservation.computerhistory.org/LISP/book/L...
"When the compiler is called upon to compile a function, it looks for an EXPR or FEXPR on the property list of the function name. The compiler then translates this S-expression into an S-expression that represents a subroutine in the LISP Assembly Language (LAP). LAP then proceeds to assemble this program into binary program space. Thus an EXPR, or an FEXPR, has been changed to a SUBR or an FSUBR, respectively. Experience has shown that compiled programs run anywhere from 10 to 100 times as fast as interpreted programs, the time depending upon the nature of the program. Compiled programs are also more economical with memory than their corresponding S-expressions, taking only from 50 per cent to 80 per cent a s much space.' The major part of the compiler is a translator or function from the S-expression function notation into the assembly language, LAP. The only reasons why the compiler is regarded as a pseudo-function are that it calls LAP, and it removes EXPRts and FEXPR1s when it has finished compiling."
Enough said.
Which is basically what people liking dynamic languages have said all the time - types is only good as long as the overhead they bring doesn't cost more.
I would quibble though that Python's is actually pretty good at this point, and, despite what the below poster is saying, straight-forward to set up and use. I am still perplexed that the author chose Python over Rust or Scala or TypeScript though, especially given they presumably want to migrate a Haskell codebase.
I'd be interested in hearing/discussing more about this. I was very surprised, when I embarked on my side project, that Rust's options for SQL ORMs all seem so weird.
I think what you are referring to is the derivation of FromRow and stuff with SQLx, right?
This means you have rock solid assurance your program won't have database syntax errors at runtime. Basically it's just one less thing to worry about, and it's ideally suited for LLM-generated code.
I remember that being one of its "pros" when I was going over options with the LLM, but now, I just have a bunch of raw SQL strings in my codebase and sqlX's main use in that project, off the top of my head, is just instantiating Rust objects from the raw results with `FromRow` (it's probably doing more than I realize; I am not as connected with the code as I want to be, using LLMs to move fast to launch a couple features before revisiting a lot of the mess).
Start the countdown timer for how long it takes them to discover that was a mistake.
Nothing to do with Haskell, but good grief, LLMs do not in any way, shape or form save you from the deep, unfixable problems with Python.
At the very least you need all the static checking machinery like Ruff, Pyright, and hefty unit tests that take the place of typechecking if you don't want obvious failures to only show up in production.
I had this recently with an ML training pipeline, where Python is essentially forced on us. A dynamic error occurred after 17 hours of training - something that a real type system could have easily caught.
The solution that the LLM came up to prevent this in future was a complicated Enum-based system that just made me wish I could use a real programming language.
I have the impression that Python basically wins by default in those spaces due to the lack of many good libraries in other languages (except for, like, C++).
But curious if this is just a very outdated view of the world
If you push through that you end up with code written in a language people have used for formal proof (seL4 model is Haskell) and deployment wise a binary that +/- libraries you depend on ought to be reasonably portable.
I'm very surprised anyone would want to go the other way. Same ecosystem pain, plus you need to start shipping interpreters or containers, plus the language just doesn't really compare.
It could just be a coincidence of statistics, but it does cover every Haskell user I know (needless to say, these people are much smarter than I am).
Because a lot of times it's missing the way of making the needed steps for the conversion (or it's just not obvious)
Sometimes it needs some nudging
Also this comment is a bit generic, it can also apply to cases where it's not an obvious "type check" but a redundancy that needs to exist but the AI can't get around
I agree that short compilation times are very desirable, but I do not see why Python must be the solution for that.
I do not know whether Haskell can be compiled quickly, but from my experience, I am very certain that short compilation times are easily achievable for languages with good static type checking, especially with compilers that have different options that allow choosing between fast compilation and heavily optimized compilation.
An optimized compilation may require a much longer time than a fast compilation, but that has no relationship with the programming language used in the source text, but only with the intermediate representation used by the compiler and the target CPU ISA.
When you try to optimize a project by generating many variants with a LLM, I doubt that all those variants will be generated from scratch, completely independently, even if only for the reason that when using a commercial LLM the cost of a completely new variant will be much higher, by requiring many more tokens, so whenever possible it is preferable to generate other variants by just patching previous variants.
Whenever a variant is generated by editing a previous variant, incremental compilation can be used, which should be pretty much instant on modern computers.
There is also this
> How do we make library docs full of copy-pastable, realistic examples, not just beautiful types?
Which is useful for humans as well as agents.
Haskell indeed has a very bad track record of documenting its libraries. For many people, just having the function signatures is documentation enough.
Rust is equally bad at compile times (if not worse) but its standards for documentation is at another level
You make a fair point about a documentation gap. The first step is always defining the problem. Wanting to add lots of realistic examples sounds like a wish for more tutorial or beginner-friendly content. Do you see the problem differently?
On the other hand, a given pure function type only has only so many possible implementations — why tools such as djinn and MagicHaskeller exist or why Hoogle is actually useful, unlike the horror of searching for every `void (*)(const char *)` in C.
https://hackage.haskell.org/package/djinn
https://hackage.haskell.org/package/MagicHaskeller
https://hoogle.haskell.org/
From that perspective, Haskell docs tend to be more expert-friendly — perhaps a rationalization, granted — which seems ideally suited for an in-IDE model to help bridge between developer intent and typechecked code. However, this comes at the expense of putting in the reps to rewire the developer’s brain to think in functional terms and the resulting mind opening and horizon expansion to think new thoughts she wasn’t capable of even considering. In these days of LLMs, fretting over that particular opportunity cost may be thinking nostalgically about the loss of craftsmanship in fine, well-balanced buggy whips.
In the limit now, will all programming be strictly literate?
OCaml enters the room...
There's no reason to read the code anymore.
The LLMs produce it inhumanly fast.
They can usually debug problems faster than Haskell can compile the project.
Whatever. Let's just ship Python.
have you tried?
i use elixir and carefully watch the agents and its very seldom making typing mistakes (elixir is in-between, it's typed but only as a checker).
ultimately typing doesn't help as much because it's nonlocal information. if the system can locally infer what the shape of functions is, it's way better.
- I do not watch agents, at all. Rust and Typescript. When I use Typescript only I have some guidelines so that we build the stack to be as strict and type driven as possible.
next features are integrating a small transformer i built to codon optimize and probably "slack" to better communicate with the 2 ppl who work for me.
is your rust+typescript project at that level of complexity?
And that is not even considering how often the agent needs to run tests to get it right?
Regarding the first, I think you're probably right, but then again, if there is a 15-minute base cost, it's hard to amortize that through fewer incremental runs of the compiler.
(Which isn't to say that I think they are doing the right thing.)
Ironically, this is also what makes them shine with LLMs, the LLM has access to the running program and can modify it while it's running to get feedback instantly.
Complex type systems are cool. But, they are not free. I say this as someone who's first programming language was Haskell.
Amazing. How did that happen? Is it true that functional programming is only counterintuitive because almost everyone starts out with an imperative language?
Yes, imperative languages were initially counter intuitive to me. OO was even worse. SQL was fine as it's more declarative/functional.
How much faster? IOW, what's the difference (in minutes/seconds, not in percentages) between vibing Haskell and vibing Python?
Slow compile times should have been a deal breaker for how they impacted human coders. LLM coding just makes the problem more stark.
The amount of certainty random people have that LLMs have already revolutionized software development seems to be directly proportional to the media awareness of the AI companies finance unsustainability.
Iteration speed is not only to appease business people. As a long-time dev I prefer for my PL and its tooling to not stand in my way. I want to be enabled, not be left a scarred veteran with badges of honor like "Managed to install 5 dependencies and compile and run! And it only took me 3 hours!" or some such.
Compare to Golang, Rust, Elixir, and no small amount of others -- you add a dependency, run a command, twiddle your thumbs 30 seconds, done.
I want to solve problems. Not troubleshoot tooling that should have been prod-ready 10 years ago.
Yes, companies can get into trouble with Haskell compile times, memory use, etc., but enforcing disciplined coding rules and advances in tooling solved the majority of the problems by 2016.
We use LLMs to assist in writing Haskell and have for several years. We keep track of compile times to catch regressions and on our largest code base an agent-driven change takes an average of 96 seconds (as of this month) to compile. Agents looking to do quick compile checks during code writing can run with flags that make the compilation run in less than 5 seconds. In all cases, running out test suites takes a lot longer than compile times.
The only time this isn't true is when you compile from cold start, as you would when using multiple work trees...which is the expected behavior when vibe coding features. So yes, I believe the issues they're running into have a lot to do with vibe coding. Check out all their integrations...Haskell's probably a poor match for what they do, but vibe coding integrations is probably what they need to do to get new customers.
We want to iterate and progress. You likely are used to Haskell's tooling quirks long time ago. Everyone else outside of the ecosystem does not have that advantage.
My intuition is that type-safe languages with fast compilers are the best option. Maybe Go? I personally prefer Java just due to my experience running it in production, but am not sure there's many arguments for it over Go in a greenfield application. The other candidate would be Rust, but I worry about token efficiency and tool performance, I suspect it's not worth it for the runtime improvements.
All that being said, in this article switching to Python seems like a wild choice. Relatively poor performance, no compile time checking at all. Python's big selling point was developer ergonomics, which seems largely irrelevant now.
These are all just thoughts at the moment, I should try to find some evidence one way or another.
That is: Have a Haskell base system. Have a Python "development" version on which you iterate at lightning speed. But also, in the background, moving at whatever pace it takes, have an agent running that imports all the Python development changes into the Haskell version. Have nightly builds of the Haskell version to reap its benefits (issues caught by the type system, more efficient native code).
And it doesn't have to be Python/Haskell of course. The "development" version could be a (hypothetical?) interpreted Haskell. I have no idea if ghci would be useful for this. Neither do I know if the 15-minute Haskell build time is spent in the frontend (so an interpreter would have to pay that cost too) or in code generation or linking (which the interpreter wouldn't need to care about). Anyway, these are things I would think about before I do what the OP did.
Go is the perfect language for this new world. Its development loop is fast, it has types but not too much so, it is memory safe, it is easy to deploy and it runs efficiently enough for most line of business use.
Go was originally developed specifically for a world of interchangeable hard-working juniors, which is exactly what LLMs can scale up with only your budget as the constraint.
I'm wondering what advantage a hypothetical faster-compiling-Haskell would have in that world.
I find Ruby a very beautiful language, and Rails is an excellent web framework, but I need typed functions, record types and sum types.
They help not just with correctness, but also as living documentation that lets me understand AI generated code. TypeScript provides discriminated union, but not exhaustive pattern-matching, and its syntax is a bit verbose, but since I'm no longer writing most of the code myself, I can live with it.
However I can't imagine using Python or any other dynamic language going forward. There is likely good reason for you to choose it, and I'm curious to know what that is.
this is undoubtedly true.
LLMs have made me move away more from python rather than into it. I'm very surprised by this experiences of the author. The article is all over the place as well. Going basically all in on Python because it is apparently better than Haskell for LLM use and than agreeing with someone that says Rust is the best.
- The benefits of more "extreme" type systems are more accessible and valuable than ever. I have a fairly involved project built on Lean that I hope to open source this month, and it's been a joy to work in even for uses outside of mathematics.
- Readability, build time, infra complexity, and everything that affects your speed after finishing your implementation--these things now matter more than ever.
It's sort of a dual ergonomics problem, in some sense. And given that, the author's lament makes complete sense to me, especially:
"An AI-enabled Haskell ecosystem would ask different questions. How do we make Haskell easier for agents to use well? How do we get more high-quality Haskell examples into model training data? How can we scale reviews? How do we make library docs full of copy-pastable, realistic examples, not just beautiful types? How do we make project bootstrap fast? How do we make error messages more agent-friendly? How do we reduce cold build times? How do we make common industrial patterns obvious to a model that is trying to help?"
As much as I respect this guy who tried to work and push an alternative ecosystem, it's hard for me to shake off the impression that, rather than due to Haskell compile time, he moved to python because it's easier to find developers for it and it's the de facto scripting language for LLMs.
No problem about that, of course. Running a company is hard enough, I think that passion and idealism for a language/platform/technology out of aesthetic appreciation can only go so far and after a certain age just making money and reaching your professional objectives count more.
Fast compile times is one of the most important qualities for developer productivity. It made Haskell a non-starter for many developers even before LLM driven development took off.
We need more general purpose Elm languages in the space.
Clothes became chain-stitched (and later lock-stitched) because machines could chain-stitch. If there were a super-efficient hammering machine, it could be better to figure out ways to use nails to replace screws in designs than to hold onto screws just for nostalgia's sake.
How about for the sake of the bridge continuing to stand? Or is that not a good enough reason for the accountants?
For some time now it’s felt clear (or at least extremely) compelling that agents need fast compile times in order to be effective, especially when you’re working in parallel. But the other thing that has felt just as obvious is that agents need strong type systems and narrow guardrails in order to constrain their outputs. These two things felt clear enough to me that, like the author, I wanted to choose a language ecosystem that maximized them. There _are_ languages that both have expressive type systems _and_ fast compile times. I wonder if the author investigated any of them, before deciding that no compilation time at all was acceptable.
In my case I landed in OCaml. I think there are other options in the space—Go if you want less typing but faster compiles; Rust if you want more types but slower compiles. My mostly vibes-based evaluation landed on OCaml, and I’ve been pretty happy with the results.
I read the second paragraph as saying close to the opposite of that, particularly "the model can often avoid the mistake before the compiler ever sees the code. And as the models get better, the relative value of catching every possible issue at compile time changes."
In other words, LLMs are much less likely to make dumb fat-finger mistakes than humans, ergo the value of type checking has fallen.
Everything in the prior sentence is, obviously, highly debatable.
- None of us are experts in Rust, and we're all solid at Python.
- Rust felt like an under-correction for what we wanted (get all friction in front of the LLM out of the way).
- Our high-performance stuff is not being migrated at this time (Scarf Gateway), so we're just talking about basic CRUD backends here. Basically any language will work.
Whereas Python just interprets and gets off to the races.
Feels like we had this discussion years ago as humans..the false promise of dynamic languages.
it's a shame scarf is struggling so much they are pinching pennies :/
you don't need to be, you can learn Rust or whatever way-better-than Python language as you use it with an LLM! it's an amazing process.
https://github.com/microsoft/typescript-go
But as others said: if Haskell was not giving you too much in terms of excellent typing system then maybe going for a PL with a much faster compiler like Golang would have been best for you.
I also went through quite a process to select a language to work with LLMs[1] before settling on OCaml.
I am not unhappy with the choice and find it works quite well, with relevant skills loaded, but I am always interested in others’ experience and understanding what they’ve discovered works well.
[1] https://news.ycombinator.com/item?id=48104481
I have a pretty elaborate harness setup, I use my own spec DSL to plan and execute code. So what works for me may not totally apply to others.
I haven't used the frontier models in a few months, so I can't speak at all to this latest batch. When I switched over from frontier I found that what I lost in training data, I more than made up for with Pi harness.
Your process seems more thorough than mine. Mine was literally "I like camels". Switching over to OCaml I found wonderful - I had not worked with FP before and I found it delightful, it kind of cured my programming burnout a bit. I hand built a CRUD app in OCaml/Dream but still feel like a rank beginner, especially when it comes to actually writing code, lol. But I'm getting much better at reading it.
I think the typing and structure of OCaml is a great fit for LLMs, but also the proximity to other things like DSLs, formal proofs, and algebraic structures has tons of untapped potential.
I'm having an absolute blast with it. I like programming again.
On the contrary, for a small rust project, I had to clean out 180gb of cargo nonsense from the last ~3 days worth of compiles on a single, narrowly focused topic branch.
The library situation might be funky, but I'm also learning Lean 4 by hand. The tooling & lsp integration is lovely.
*https://signalsandthreads.com/performance-engineering-on-har...
At least from the sidelines, it looks like they've built/forked most parts of the OCaml ecosystem to make it work (OxCaml, Dune was built by then, custom profilers, custom networking hardware, etc).
Of course a lot of their work is open source and can be used, but I wouldn't assume that it would be worth the effort to do something similar with OCaml yourself. Maybe it is! LLMs go brrr and all that.
This feels like someone giving a great commencement speech, then proceeding to pull out a gun and shoot himself in the foot as the final act.
[0]: https://discourse.haskell.org/t/after-7-years-in-production-...
if my CEO wrote this article, I'd quit [1] in an instant
[1] "quietly" while i found gig+1. oh and the private out-of-band engineering gossip and trash talk would surely be hilarious
Since Opus 4.6, LLMs have been pretty clever at using fancy types with libraries like Servant and Beam. The expressiveness of the types, combined with feedback from the compiler, means that agents converge quickly to something that works. I don't think I've noticed agents having to run the compiler so often that compilation speed is an issue.
Haskell compiles slowly, but we have not found that to significantly inhibit AI-supported dev so far. We use ghci with `-fobject-code`, and our largest leaf module (10k lines of webserver request handlers) takes 8 seconds to `:reload`. To run stuff in it, the agent pipes `:reload`, or other invocations, into `ghci`.
Working on parts parts that are early in the module chain, such as our Prelude, makes the whole-project `:reload` take 50 seconds. Much of that could be avoided if we didn't suffer from the TH recompilation problem (https://gist.github.com/nh2/14e653bcbdc7f40042da3755539e554a). Originally I made a small GHC patch to hack that out (what this conservative recompilation protects from cannot happen in our project), which made the reload much faster, but the logic was changed in recent GHC so that doesn't work anymore.
In C++, we have some individual files (and thus compilation units) that take 45 seconds to compile.
Python is of course the fastest to (build+run). However, Python also has some problems with repeated runs: Once you have many imports, just starting the program (e.g. `--help`) takes ~2 seconds resolving imports. Do `import pytorch`, add another 2 seconds. For repeated runs, this can be a pain; Haskell and C++ are much faster for that, so they win when you don't have to change the code but repeatedly use already-compiled tools in a different way.
In all 3 languages, getting things to typecheck is very fast, because the agent directly reads the vscode LSP typechecking errors which are < 1s feedback. Claude Opus 4.6-4.8 understand all 3 languages very well.
I agree that GHC devs should focus most of their efforts on compile speed, and real-world pain solving. Some parts of that are indeed being done (e.g. newest GHC can write bytecode to disk to make the above workflow much faster, and codegen is by far the slowest part of compilation). But I think the focus should be even more on that. I consider most important and unsolved:
All of these have open GHC tickets that I think should be the highest focus.Other real-world production things are being solved quite nicely currently:
I get your general point that if iteration speed (as in change code + run, 100s of times a day) is your highest value, Python does quite well. We intentionally chose Python for the part of the codebase that's relatively simple data importing but from 50 different sources, count growing adding new sources all the time, and need just quickly iterate on each until we got it.But I find it would not be a great language for the other parts. Its concurrency story is bad, its interpreter is very slow and immediately disqualifies Python when you need to do e.g. a line of code for every 1000 Bytes (say for streaming small data chunks), typing is very useful but bolted-on and not always correct.
Things where Haskell remains best-in-class is anything nontrivial-webservers, I/O, program correctness, the main "general purpose" programming, refactoring and long-term maintainability.
Who cares about performance.
If you go from taking 2 days to write some code and 20 minutes to type check (which does seem long, don't get me wrong, but still) to 10 minutes to prompt some code and 20 minutes to type check, that percentage increase to me isn't enough to justify switching.
You're still almost 2 days ahead, and converting those 20 minutes to 20 seconds are not going to make you ship a feature appreciably faster. But those types stand strong and I don't believe they can yet be replaced by an LLM believing they're correct.
Having said that, I also think that Haskell should massively speed things up. Having strong types if nothing else should surely produce some amazing type-checking speed wins.
Another obvious point is that an industry that runs on code slop will stagnate in terms of language an human tooling design.
I would like to add one additional observation, since we have been using both Haskell and Python in production for a long time:
Haskell excels at platform work, while Python excels at product work.
Our infrastructure teams work in Haskell (and also Rust nowadays), while our product teams work in Python. This gives us the best of both worlds (in my opinion): fast and rock-solid infrastructure on the platform side, and fast development speed and quick iteration cycles on the product side.
This setup has worked well for years for us, but it remains to be seen how and if this is going to change as well in the new AI era.
> The model can often avoid the mistake before the compiler ever sees the code.
> The type safety we gave up hasn’t been noticeable in any concrete way yet [...]
> Type safety can be a huge advantage for LLM-generated code if the compiler is helping the agent converge quickly.
Well, good to have this question cleared up once and for all :-)
Everything sort of exists, but it's this heinous zero documentation, high pain tolerance thing: buck2 and RBE with NativeLink and hooking that up to action runners and it needs to all work in a container or on nix or in a deb and on MacOS, you hand roll the auth and the certs and where do your compilers come from, can it do NVIDIA, can it do mobile.
Problem is switching off Haskell doesn't help for long: the agents proliferate and you're back where you started with more bugs. So I've been sucking it up and getting all this shit one click and it works. This is good enough for my use, and if the Scarf folks want a solution and are willing to work with a garage band startup, I'd be open to doing a closed alpha. I have a buck2 where you write the rules in Haskell (if you even need to change the prelude, it ships with a WASM that isn't coupled to fbcode), and the Nix cache/substitutor is backed by NativeLink so it scales to anything and it speaks all the protocols correctly and with a verified supply chain.
I'm not even really sure this will become a product, I just need it, but I sort of suspect others will need it too. If there's interest I'll put up a landing page with an email sign up thing.
We're a Haskell shop (and have been for over 10 years now) and are finding agentic development with Haskell to work pretty damn well.
Cold compile times in Haskell are painful indeed. Our development practices don't really cause us to do that much - even with agents.
It's unclear to me if the development practices at Scarf that cause them to hit this pain often are worth it if it means giving up Haskell because the compile times are too bad. Maybe they are, but I don't think so.
It not that it didn't work at all, so much as that it worked much better in other languages when we compared side-to-side. It's possible we had dev practices that could have been modified further to suit Haskell better (we had been doing that for years already), but increasingly I want the toolchain to adapt to us, not the other way around.