RSSAmplifier

Cecil Woebker · Jul 31, 2026

What OCaml Taught Me About Engineering

0
Sign in to vote or save

Cecil Wöbker · Cecil Wöbker

Back at TUM I had to take a compulsory course called Informatik 2. The structure was a bit unusual. Half of it was OCaml, and the other half was about formally proving that a program does what it claims to do. The first lecture opened with a list of really expensive software bugs (rockets exploding, airbags not deploying), and the rough pitch was that there is a discipline for avoiding that kind of thing. I remember walking out of that one pretty intrigued.

Then the first problem set arrived and I was totally lost. It wasn’t that the material was hard, more that the whole shape of the language felt unfamiliar. There were no loops. You couldn’t reassign a variable. A functor turned out to be something completely different from what I’d assumed when I first read the word. It took me a bit to get comfortable with the basics.

I made it through the course and didn’t write a Hoare-logic proof in anger after that. But the framing has stuck with me longer than almost anything else I learned at university: that correctness is something you encode into the structure of a program rather than something you only check for with tests afterwards. It was a bit like taking a lockpicking class. Even if you never pick a lock after, the way you look at locks is different.

That was over a decade ago. A few years after the course I co-founded remberg, and we write a lot of TypeScript now and not OCaml. But the things that course drilled into me have shaped how I think about software design and what I tell engineers when asked why we invest so heavily in types.

The Billion-Dollar Mistake#

In 2009 Tony Hoare gave a talk at QCon London and called the null reference his biggest regret. Hoare invented Quicksort and won the Turing Award, so when he says something like that it’s worth pausing on.

“I call it my billion-dollar mistake. It was the invention of the null reference in 1965. My goal was to ensure that all use of references should be absolutely safe. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement.”

— Tony Hoare, QCon London 2009

By that point null had been causing crashes and silent data corruption in production systems for around forty years. The ML family had been making the same point in the background since the 70s. Standard ML didn’t have a null reference; neither did Haskell when it appeared in 1990, and OCaml shipped without one in 1996. The engineers using these languages didn’t seem to miss it.

OCaml never had a null reference. If a value might not exist, you have to say so explicitly:

type 'a option =
  | Some of 'a
  | None

To use a value wrapped in option, you have to pattern-match on it. If you forget the missing case, the compiler flags it right away (a warning by default, and any serious build config promotes it to a hard error). You can’t write a null pointer dereference in OCaml in the first place.

A bunch of modern languages have been adding null-safety in recent years. Kotlin, Swift, Dart 3, TypeScript in strict mode, the nullable reference types in C# 8 — most of these are retrofitting an idea that OCaml had built in from the start in 1996. It took the rest of the industry quite a while to come around to it.

Make Illegal States Unrepresentable#

There’s a phrase you hear a lot in functional programming circles, originally from Yaron Minsky who ran the tech side of Jane Street for years: make illegal states unrepresentable. The idea is fairly simple: use the type system to make sure that invalid combinations of your data can’t be expressed in the first place.

If quantity and amount are both floats but mean different things, don’t write them as float. Write Quantity and Amount as separate types and let the compiler refuse to multiply them. If a user can be authenticated or not, don’t model it as a boolean flag on a single type. Use two separate types and make it structurally impossible to call methods that need an authenticated session on an anonymous one.

type order_status =
  | Pending
  | Processing
  | Shipped of tracking_number
  | Delivered of delivery_timestamp
  | Canceled of cancellation_reason
let describe status =
  match status with
  | Pending -> "Waiting to be processed"
  | Processing -> "Being prepared"
  | Shipped t -> "In transit: " ^ t
  | Delivered ts -> "Delivered at " ^ format_time ts
  | Canceled r -> "Canceled: " ^ r

If you add a new status to the type, say RefundRequested, the compiler flags every pattern match in the codebase that doesn’t handle it. That’s basically the type system doing the code review for you.

It’s the verification half of the course showing up in everyday work, with the formal-proof rigor dialed way down. I never wrote a real Hoare triple after that semester, but I write discriminated unions all the time. The instinct is the same: put correctness into the shape of the program rather than into a test suite that runs against it.

When the Tool Fits the Problem: Compilers and Type Checkers#

There’s one domain where functional languages with algebraic data types are basically the default choice, and the industry keeps independently arriving at the same conclusion: building compilers, interpreters, and type checkers.

Meta’s JavaScript type checker, Flow, is written in OCaml. So is Hack, their PHP type checker, and Pyre, their Python type checker. That’s three type checkers at one company, and once the first team had proved OCaml out, the next two saw little reason to choose differently.

A type checker is essentially a program that walks a tree of source code and classifies what it finds: a FunctionCall node, a BinaryExpression node, a TypeAnnotation node, and so on. In an object-oriented language you usually end up with a visitor pattern and a class hierarchy. In OCaml it’s just an algebraic data type and a match expression:

type expr =
  | Literal of value
  | Variable of string
  | FunctionCall of string * expr list
  | BinaryOp of operator * expr * expr
  | IfExpr of expr * expr * expr
let rec type_of env expr =
  match expr with
  | Literal v -> type_of_value v
  | Variable name -> Env.lookup env name
  | FunctionCall (name, args) ->
      check_call env name args
  | BinaryOp (op, left, right) ->
      check_binary env op left right
  | IfExpr (cond, then_e, else_e) ->
      check_if env cond then_e else_e

Every case is explicit, and if you add a new AST node and forget to handle it in type_of, the compiler complains right away rather than the issue showing up much later as a strange production bug.

This matters to me in a fairly practical way. At remberg we have a pretty large TypeScript codebase by now (north of a million lines, with the team that maintains it having grown a lot over the past couple of years). We lean fairly heavily on TypeScript’s type system: discriminated unions, exhaustive switches with never checks, branded types for domain primitives. TypeScript is doing a weaker version of what OCaml does natively, but the underlying philosophy is similar.

There’s also an angle to this that has become much more relevant lately, which is that well-typed code ends up being a kind of contract for AI tooling on top of being a contract for your colleagues. When the types are vague or missing, the assistant working in your codebase tends to fill the gap with plausible-looking nonsense, and you end up paying for it later.

OCaml shaped how I think about this because it doesn’t really give you an escape hatch. There’s no any, and no implicit undefined floating around. TypeScript does give you escape hatches and the temptation to reach for them is always there, especially when you’re in a hurry. Understanding why OCaml made the stricter choice — and what you give up when you reach for any — is what keeps me from reaching for it most of the time.

The Real-World Proof Points#

I find the organizational arguments for functional languages more interesting than the theoretical ones, because companies actually betting their business on a language tell you something about how the tradeoff plays out in practice.

Jane Street, the quantitative trading firm, runs basically their entire operation in OCaml — trading systems, research infrastructure, monitoring, accounting. The reason they give is that the correctness and productivity they get from OCaml lets a relatively small engineering team handle billions of dollars of daily trading volume without too many surprises. The tradeoff is that they accept some raw latency ceiling vs writing it in C++, but for them the reduction in bug rate is worth it.

Ahrefs built one of the largest web crawlers in the world in OCaml. Their index covers somewhere around 500 billion pages and they have around 1.5 million lines of OCaml in production, with systems built years ago that still just run without much intervention. The type system catches a lot of data format issues at compile time that in a dynamically typed system would have shown up as silent failures hours into a multi-day crawl.

Semgrep upgraded their static analysis engine from OCaml 4 to OCaml 5 and saw as much as a 3x speedup in scan times on large repositories. Most of that comes from OCaml 5’s multicore support: threads sharing a single memory heap instead of the forked subprocesses they had to parallelize with before. Performance used to be the standard objection against functional languages, and that’s been gradually shifting.

Where It Doesn’t Win#

I should also be clear about where this argument breaks down, because every “right tool for the job” pitch needs the caveats spelled out alongside it.

We don’t write OCaml at remberg, mostly because the hiring pool just isn’t there and it really is trickier to build a full-blown SaaS solution with it. You can build an OCaml team if you’re Jane Street and can hire for raw aptitude and then train the language internally. For a company growing fairly quickly, requiring OCaml fluency would narrow the candidate pool in a way that creates real operational risk.

The ecosystem is uneven too. OCaml’s standard library is deliberately small, documentation quality varies a lot, and you sometimes end up reading library source code to figure out how something behaves. Compared to npm or cargo it’s a noticeable difference.

Functional programming also isn’t the right answer for every kind of problem. For CRUD web applications with a large team already fluent in existing frameworks, the tooling and the developer productivity almost always win over the theoretical type-system advantage. For mobile apps, machine learning workloads, or game development, the dominant ecosystems are elsewhere and OCaml just isn’t a serious option.

The question I find more useful than “is FP better?” is whether the constraints of the system you’re building actually line up with what functional languages do well. Things like correctness that genuinely can’t be tested after the fact, or a domain that maps naturally to data transformation, or a small team trying to maintain a fairly large surface area over many years.

The Mindset Outlasts the Syntax#

I haven’t written any OCaml since that course, and the total amount I ever wrote was probably around five hundred lines. But the patterns it taught me have shown up in basically every system I’ve worked on since: model the domain in types, eliminate null at the boundary, make exhaustiveness compile-enforced, treat the type system as documentation that doesn’t go stale.

TypeScript has been absorbing functional programming ideas for years now. Template literal types, discriminated unions, exhaustive switches, the unknown / never distinction. The engineers at Microsoft have been pulling OCaml and Haskell ideas into the most widely used language in web development for over a decade. Many TypeScript developers might not know that the discriminated union they’re writing today was an ML idea from the 80s, and they don’t really need to. But if you do know where it came from and what it was originally meant to solve, you tend to use it a bit differently.

For me that is in the end the lasting value of learning OCaml in a university course. Not really the syntax or the specific language, more the mental model of what a type system is actually for and what you’re giving up when you don’t take it seriously.

The practical question for any project I’ve worked on is whether the domain constraints are encoded where the compiler can enforce them, or whether they only get caught later by the test suite and the 2am incident review.

There’s obviously a lot to dive deeper into in the age of AI and what it means if the assistants can read your types alongside your colleagues. But that’s a post on its own.


If you want to dig deeper into any of this, Yaron Minsky’s talk “Why OCaml” and his essay “OCaml for the Masses” are good starting points, and Scott Wlaschin’s “Domain Modeling Made Functional” takes the type-driven design ideas further. The TUM course itself has been renamed since I took it; the WS 2018/19 iteration is the closest public version to what I studied.

Read the original on cwoebker.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.