OCaml, learning log
// status: seedling
A running log of things I pick up while working through Real World OCaml and poking at the language. Notes are deliberately rough — see the garden conventions. Coming from a Rust and Haskell background, a lot of the ideas land quickly; the friction tends to be in the corners — tooling, syntax conventions, and a few words that mean subtly different things than they do elsewhere.
The module system
The single feature that makes OCaml feel most itself.
- Every
.mlfile is implicitly a module.foo.mlis the moduleFoo— capitalisation is not cosmetic, the compiler genuinely derives the module name from the filename. - An optional
.mlifile acts as the interface/signature. What is not exposed in the.mliis truly private — stronger than Rust'spub/pub(crate)dance. - Modules can nest, be anonymous, be passed as first-class values, and be parameterised (→ functors).
= The corresponding signature:
= open brings names into scope; include copy-pastes them into the current module's signature. Mixing them up is the first intermediate-level foot-gun.
Variants & pattern matching
Variants are algebraic data types, same idea as Rust enum / Haskell data:
pi *. r *. r
s *. s
w *. hThings worth remembering:
functionis sugar forfun x -> match x with. Reach for it when the last argument is the thing you're matching on.- Exhaustiveness checks are on by default but warnings, not errors. Promote them with
-w +8or[@warning "+8"]— otherwise a missing case silently compiles. whenguards let you add boolean conditions to a branch, but they opt out of exhaustiveness checking. Use them sparingly.
Polymorphic variants
The lesser cousin — no declaration needed, tagged with a backtick:
if n > 0 then `Positive
else if n < 0 then `Negative
else `ZeroHandy for throwaway sum types and for library APIs that want to be open to extension. The cost is worse error messages and a slightly more complicated type scheme. Default to regular variants; reach for polymorphic ones when the extensibility is genuinely worth it.
Functors
Parameterised modules. Given a module, produce a module.
=
=
= MakeSet This is how the stdlib's Set.Make / Map.Make work. It feels heavyweight compared to Haskell typeclasses or Rust traits, but the upside is explicitness — there is exactly one compare in scope and you can see where it came from. For ad-hoc polymorphism you can also pass first-class modules, which blurs the line with dynamic dispatch.
Tooling — opam, dune, utop, merlin
The ecosystem used to be rough. It isn't any more.
- opam — package manager. Think
cargocrossed with a Python venv: it installs compilers and libraries, and supports per-project switches (opam switch create . 5.1.1). - dune — the de-facto build system. A
dune-projectat the root plus tinydunestanza files per directory. Ludicrously fast incremental builds. - utop — the good REPL. Tab completion, multi-line editing, and the thing you actually want instead of vanilla
ocaml. - merlin + ocaml-lsp — editor brains. Types on hover, jump-to-def, completions. Configured via a
.merlinfile historically, these days dune generates what's needed. - ocamlformat — the formatter. Like
rustfmt, with configurable profiles. Just commit a.ocamlformatat the project root and stop arguing. - odoc — doc generator. Doc comments are
(** ... *)(double asterisk).
A sensible starter combo: opam for the switch, dune for the build, utop for scratch work, ocaml-lsp + ocamlformat wired into the editor.
Things that tripped me up
The ones worth writing down so I don't hit them twice.
=vs==— exactly backwards from most languages I use.=is structural equality,==is physical (pointer) equality.<>and!=mirror them. Default to=.;;— only needed in the REPL to end a top-level phrase. In a.mlfile it's almost always a smell.- Labelled and optional arguments —
~nameis labelled,?nameis optional (with an implicitoptionwrapper). Order of labelled args at the call site doesn't matter, which is nice, but partial application interacts with labels in ways that can surprise you. - No typeclasses —
show,eq,comparedon't fall out for free. Either derive them with[@@deriving show, eq](viappx_deriving) or use a functor. The module-first mindset feels alien at first and then obvious. Printf/Formatformat strings are type-checked at compile time —%dfor int,%sfor string,%afor custom printers. Great once you know, confusing when the error message points at a literal string.- Mutation is allowed and normal —
ref,mutablerecord fields, arrays. OCaml isn't Haskell; pragmatic mutation is idiomatic when it's local. Don't fight it. - The
Stdlib↔Coresplit — Jane Street'sCoreis a richer standard library used heavily in Real World OCaml. It shadows a lot ofStdlib. Decide early which world you're in; mixing them is a papercut generator.
To revisit
- [ ] GADTs in anger — I know the theory, haven't reached for them in OCaml yet.
- [ ] Effect handlers (OCaml 5) — algebraic effects in the stdlib, curious how they compare to what I've seen in other languages.
- [ ] First-class modules as a replacement for typeclass-style polymorphism.
- [ ]
ppxrewriters — how deep is the rabbit hole.
Back to Notes · compare modules, traits, and typeclasses · try GADTs in a typed evaluator · explore OCaml 5 effect handlers · Home.