./jerome{m}

~/Notes/OCaml - learning log.md · note · seedling · 4 min · updated

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 .ml file is implicitly a module. foo.ml is the module Foo — capitalisation is not cosmetic, the compiler genuinely derives the module name from the filename.
  • An optional .mli file acts as the interface/signature. What is not exposed in the .mli is truly private — stronger than Rust's pub/pub(crate) dance.
  • Modules can nest, be anonymous, be passed as first-class values, and be parameterised (→ functors).
ocaml
module Counter = struct
  type t = { mutable n : int }

  let make () = { n = 0 }
  let incr c = c.n <- c.n + 1
  let get c  = c.n
end

The corresponding signature:

ocaml
module type COUNTER = sig
  type t
  val make : unit -> t
  val incr : t -> unit
  val get  : t -> int
end

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:

ocaml
type shape =
  | Circle of float
  | Square of float
  | Rect   of float * float

let area = function
  | Circle r       -> Float.pi *. r *. r
  | Square s       -> s *. s
  | Rect (w, h)    -> w *. h

Things worth remembering:

  • function is sugar for fun 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 +8 or [@warning "+8"] — otherwise a missing case silently compiles.
  • when guards 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:

ocaml
let classify n =
  if n > 0 then `Positive
  else if n < 0 then `Negative
  else `Zero

Handy 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.

ocaml
module type ORDERED = sig
  type t
  val compare : t -> t -> int
end

module MakeSet (Ord : ORDERED) = struct
  type elt = Ord.t
  type t   = elt list   (* toy implementation *)

  let empty = []
  let add x s = if List.exists (fun y -> Ord.compare x y = 0) s then s else x :: s
end

module IntSet = MakeSet (Int)

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 cargo crossed 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-project at the root plus tiny dune stanza 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 .merlin file historically, these days dune generates what's needed.
  • ocamlformat — the formatter. Like rustfmt, with configurable profiles. Just commit a .ocamlformat at 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 .ml file it's almost always a smell.
  • Labelled and optional arguments — ~name is labelled, ?name is optional (with an implicit option wrapper). 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, compare don't fall out for free. Either derive them with [@@deriving show, eq] (via ppx_deriving) or use a functor. The module-first mindset feels alien at first and then obvious.
  • Printf/Format format strings are type-checked at compile time — %d for int, %s for string, %a for custom printers. Great once you know, confusing when the error message points at a literal string.
  • Mutation is allowed and normal — ref, mutable record fields, arrays. OCaml isn't Haskell; pragmatic mutation is idiomatic when it's local. Don't fight it.
  • The Stdlib ↔ Core split — Jane Street's Core is a richer standard library used heavily in Real World OCaml. It shadows a lot of Stdlib. 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.
  • [ ] ppx rewriters — 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.