Dennis Lustre

Software Engineer

I design and build with equal care for how it works and how it feels.

First engineer @ Fuerte. Part-time MS in CS @ Georgia Tech.

last updated 08-02-2026
← back to writing

My experience using OCaml for 'Crafting Interpreters'

I had already worked through Bob Nystrom's Crafting Interpreters once, in Go. Doing it a second time in OCaml wasn't because the first one went badly. It was because pattern matching over an AST is the thing OCaml was built for, and I wanted to find out how much of the book's structure was really about interpreters and how much of it was about Java.

The answer turned out to be "more about Java than I expected," and I found that out by walking into a wall.

Classes first, and then curiosity

I built the parser and the interpreter as classes to start with. This worked completely fine. I want to be clear about that, because what follows is a story about abandoning something that wasn't broken.

But OCaml has first-class modules, and I was curious what the same code would look like if the parser and interpreter were modules I could pass around as values. So I rebuilt them that way. At that point I had only implemented block scoping, and the rewrite went smoothly.

I still think functors and modules-as-variables are genuinely interesting. There's something in there that feels like a marriage between object-oriented programming and metaprogramming -- you get to hand a whole namespace to a function and let it decide what to do with it.

Then I tried to implement functions, and it fell apart.

The wall

Two things went wrong at once.

The first is that environment values -- variables, functions, all of it -- need to be generic. The second is that expressions need to be able to evaluate to functions.

Nystrom doesn't have a problem here, because Nystrom is writing Java. He evaluates an expression and type-casts the result to a callable. That's it. That's the whole solution.

I had represented evaluation results as variants, which felt like the obvious OCaml move. So I tried to hammer in a Function variant, and got stuck almost immediately. The reason is that a function's call() needs an interpreter -- and the interpreter is transitively dependent on the evaluation result type, through the environment module. Which means the environment, the interpreter, and the result type of evaluate are all mutually recursive with one another.

I could not find a way to say that. If OCaml has a way to define a module type that's mutually recursive with a type, I never found it, and I'm fairly convinced that's the piece that would have made the whole thing work.

Everything in one file

The compromise, if you want something close to Nystrom's design, is that a lot of the types have to live in the same file. Here's the cycle that forces it:

expr -> stmt -> interpreter -> expr_literal -> expr

I can't put stmt in its own file, because that chain closes on itself. In Java this is a non-issue; classes reference each other freely and nobody thinks about it. In OCaml the module system is doing real work, and it won't let you pretend a cycle isn't there.

I don't think this is OCaml being obstinate. It's more that Nystrom's design leans on a property of Java that I had never once had to notice.

Variants over option fields, and why I'd take that back

Early on I went all-in on variant types instead of option fields. My reasoning was that variants are nominal rather than structural, and nominal felt more idiomatic, so it seemed like the right instinct.

In retrospect I think option fields are easier to reason about, at least when they apply. Variants make you name every possible shape up front, which is great when the shapes are genuinely different things and tedious when the only real question is whether something is there.

This is the kind of thing I couldn't have gotten from reading about the language. I had a prior about what "idiomatic OCaml" meant, and the only way to test it was to write enough code for it to be wrong.

The error messages

OCaml's error messages can be genuinely undecipherable.

Something as minor as a missing paren can flood the editor with red and produce a message about something that doesn't appear to relate to the code you wrote. As someone using the language for the first time, this was the most tiring part of the whole project -- not the type system, which I liked, but the experience of being told I was wrong in a language I couldn't parse yet.

My guess is that this is close to inevitable. Type inference is constantly trying to work out what you meant, and when you hand it something malformed, it follows that guess a long way before giving up. The error you get is a report from wherever it ended up, not from where you made the mistake.

Learning it was its own problem

I'm used to a particular way of learning a language: hit something obscure, search it, land on Stack Overflow or ask a model, move on. That loop works extremely well for JavaScript or C++, where the corpus is enormous.

For OCaml it felt about as shabby as trying to learn something brand new like Gleam or Odin. Not because OCaml is new -- it obviously isn't -- but because the number of people writing about the specific thing I was stuck on is small.

I did eventually find the manual very helpful, and I should have gone there first. The reason I didn't is a little embarrassing and probably common: it defines syntax in BNF rather than in code examples, and I learn from examples. So I bounced off it, spent longer than I needed to in search results, and came back to it later than I should have.

What I actually got out of it

The second implementation is where the learning was, and it wasn't the part I expected.

Writing Lox in Go taught me about interpreters. Writing it again in OCaml taught me which parts of the book were about interpreters and which parts were about Java's willingness to let you cast a value and move on. Those aren't the same lesson, and I only got the second one because I'd already had the first.