Output & Running
Hello, World
io:format("Hello, World!~n"). let () = print_endline "Hello, World!" Erlang prints with
io:format/1 and the ~n directive for the newline. OCaml has no single required entry point at all — a source file is a sequence of top-level bindings evaluated in order, and let () = ... is simply a binding whose left side is the unit pattern, run purely for its side effect.Printing a value for inspection
Value = {ok, [1, 2, 3]},
io:format("~p~n", [Value]). let () =
let value = Some [1; 2; 3] in
Printf.printf "%s\n" (match value with
| Some list -> "Some [" ^ String.concat "; " (List.map string_of_int list) ^ "]"
| None -> "None") Erlang's
~p ("pretty print") directive renders any term for humans, with no derivation step required. OCaml has no universal pretty-printer built into the language itself — Printf.printf requires format specifiers matched to concrete types, so printing a structured value like an option-wrapped list means writing the formatting logic by hand (or reaching for a library like Format/Fmt with derived printers), a real ergonomic gap from Erlang's ~p.Dynamic vs. Static Types
Runtime checking vs. compile-time checking
% No type declaration anywhere — this only fails when it actually RUNS:
Add = fun(X, Y) -> X + Y end,
io:format("~p~n", [Add(2, 3)]). let add x y = x + y
let () = Printf.printf "%d\n" (add 2 3) Erlang has no compile-time type checking at all —
Add("two", 3) would compile fine and only fail when that exact line executes. OCaml's inferred type int -> int -> int means add "two" 3 is rejected before the program ever runs, caught by the compiler rather than discovered in production.Strong inference makes annotations mostly optional
Double = fun(X) -> X * 2 end,
io:format("~p~n", [Double(21)]). let double x = x * 2
let () = Printf.printf "%d\n" (double 21) OCaml rarely needs the explicit annotations a Haskell or Rust example might show — its Hindley-Milner inference deduces
double : int -> int from how double is used, the same way Erlang never needed a type for Double at all. The difference: once OCaml infers a type, it locks it in and enforces it everywhere the function is called.Pattern Matching
A close parallel: function clauses and match
Describe = fun
Describe(0) -> "zero";
Describe(N) when N > 0 -> "positive";
Describe(_) -> "negative"
end,
io:format("~s~n", [Describe(-5)]). let describe n =
match n with
| 0 -> "zero"
| n when n > 0 -> "positive"
| _ -> "negative"
let () = print_endline (describe (-5)) OCaml's
match expression and Erlang's function clauses are close enough to read as dialects of the same idea: patterns tried top-to-bottom, a guard attached to a specific pattern (both spell it when — see the dedicated Guards section), and a wildcard _ catch-all.Destructuring a tuple
Result = {ok, 42},
{ok, Value} = Result,
io:format("~p~n", [Value]). let result = (true, 42)
let (_, value) = result
let () = Printf.printf "%d\n" value Both languages destructure a tuple directly in a binding. Erlang tuples are untyped and any arity; OCaml tuples like
bool * int are typed by position, so a mismatched destructure is caught at compile time instead of failing on an unlucky input at runtime.The compiler checks your match is exhaustive
% Erlang: an incomplete case only fails when the missed input
% actually arrives at runtime — a "no matching clause" crash:
Classify = fun(N) ->
case N of
0 -> zero;
N when N > 0 -> positive
% missing the negative case — Erlang won't warn you
end
end,
io:format("~p~n", [Classify(5)]). let classify n =
match n with
| 0 -> "zero"
| n when n > 0 -> "positive"
| _ -> "negative"
(* OCaml would emit a compiler WARNING here if the "_" case were
missing — an incomplete match is caught before the program
ever runs. *)
let () = print_endline (classify 5) This is a genuine safety upgrade. An Erlang
case missing a clause only reveals the gap when a real input finally hits it — a {case_clause, Value} crash in production. OCaml's compiler statically analyzes whether a match covers every possibility and emits a warning (elevated to an error under strict settings) for an incomplete one, catching the exact bug Erlang can only discover at runtime.Guards: when Means when in Both
The literal keyword `when` transfers unchanged
CanVote = fun(Age) when Age >= 18 -> true;
(_) -> false
end,
io:format("~p~n", [CanVote(20)]). let can_vote age =
match age with
| age when age >= 18 -> true
| _ -> false
let () = Printf.printf "%b\n" (can_vote 20) This is one of the closest single-keyword matches on the whole site: Erlang's
when Condition after a clause head and OCaml's when condition after a match pattern are literally the same English word, doing the same job — attaching an extra boolean condition to a pattern that must also hold for that branch to match.Chaining several guard conditions
Grade = fun(Score) when Score >= 90 -> $A;
(Score) when Score >= 80 -> $B;
(_) -> $C
end,
io:format("~s~n", [[Grade(85)]]). let grade score =
match score with
| score when score >= 90 -> 'A'
| score when score >= 80 -> 'B'
| _ -> 'C'
let () = Printf.printf "%c\n" (grade 85) Both read top-to-bottom as a chain of "if this, then that; otherwise keep checking" — Erlang guards attached to separate function clauses, OCaml guards attached to separate match arms. The visual rhythm is nearly identical between the two.
Tagged Tuples vs. Variants
Modeling "one of several shapes"
Shapes = [{circle, 5}, {rectangle, 3, 4}],
Areas = lists:map(fun
({circle, Radius}) -> 3.14159 * Radius * Radius;
({rectangle, Width, Height}) -> Width * Height
end, Shapes),
io:format("~p~n", [Areas]). type shape =
| Circle of float
| Rectangle of float * float
let area = function
| Circle radius -> 3.14159 *. radius *. radius
| Rectangle (width, height) -> width *. height
let () =
let shapes = [Circle 5.0; Rectangle (3.0, 4.0)] in
List.iter (fun s -> Printf.printf "%g\n" (area s)) shapes Erlang models "one of several shapes" as differently-tagged tuples, distinguished by their first element and arity at match time. OCaml's
type shape = Circle of float | Rectangle of float * float formalizes exactly this pattern as a genuine, closed variant type — the compiler knows every possible case, and a match that misses one is a compile-time warning, not a runtime surprise.Atoms vs. no-payload variant cases
Status = ok,
io:format("~p~n", [Status]). type status = Ok | Failed
let () =
let status = Ok in
print_endline (match status with Ok -> "Ok" | Failed -> "Failed") An Erlang atom like
ok is an unstructured constant from a global, unbounded namespace of possible atoms. OCaml's type status = Ok | Failed gives the same "one fixed named value" idea, but scoped and typed — a status can only ever be Ok or Failed, never an unrelated value from elsewhere in the program that happens to share the same name.Records
-record becomes a genuine OCaml record type
% Erlang records are compile-time sugar for tagged tuples — no runtime
% checking beyond field-name shape:
Point = {point, 3, 4},
{point, X, Y} = Point,
io:format("~p and ~p~n", [X, Y]). type point = { x : int; y : int }
let () =
let point = { x = 3; y = 4 } in
Printf.printf "%d and %d\n" point.x point.y Erlang's
-record(point, {x, y}). desugars to a tagged tuple at compile time — a naming convenience only, indistinguishable at runtime from any other tuple of the same shape. OCaml's type point = { x : int; y : int } is a genuinely distinct type with structural equality and immutability built in, and — unlike Erlang — the compiler will never let a point be confused with an unrelated two-field record."Updating" a record makes a new one
% Erlang: rebuild the tuple by hand, since nothing can be mutated in place:
Point = {point, 3, 4},
{point, X, _} = Point,
NewPoint = {point, X, 99},
io:format("~p~n", [NewPoint]). type point = { x : int; y : int }
let () =
let point = { x = 3; y = 4 } in
let new_point = { point with y = 99 } in
Printf.printf "%d and %d\n" new_point.x new_point.y Since neither an Erlang tuple nor an OCaml record can be mutated in place by default, "changing" one field always means producing a new value. OCaml's
{ point with y = 99 } copy-and-update syntax does this in one expression, copying every other field automatically — Erlang has no equivalent shorthand at expression level, so the same operation means rebuilding the whole tuple by hand.{ok/error} vs. Option/Result
`{ok, X}` becomes `Some x`
Lookup = fun(Key, List) ->
case lists:keyfind(Key, 1, List) of
{Key, Value} -> {ok, Value};
false -> error
end
end,
io:format("~p~n", [Lookup(b, [{a, 1}, {b, 2}])]). let lookup key assoc_list = List.assoc_opt key assoc_list
let () =
match lookup "b" [("a", 1); ("b", 2)] with
| Some value -> Printf.printf "Some %d\n" value
| None -> print_endline "None" Erlang's convention of returning
{ok, Value} on success and a bare error/false on failure is exactly what OCaml's built-in option formalizes: Some value is {ok, Value}, and None is the failure case. List.assoc_opt already returns an option, baking the convention into the standard library rather than something built by hand.`{error, Reason}` becomes `Error reason`
Divide = fun(_, 0) -> {error, divide_by_zero};
(X, Y) -> {ok, X div Y}
end,
io:format("~p~n", [Divide(10, 0)]). let divide x y =
if y = 0 then Error "divide by zero"
else Ok (x / y)
let () =
match divide 10 0 with
| Ok value -> Printf.printf "Ok %d\n" value
| Error reason -> Printf.printf "Error %s\n" reason Where
option only says "did it work," OCaml's result type carries a reason on failure — Error reason corresponds directly to Erlang's {error, Reason}, and Ok value corresponds to {ok, Value}. OCaml even reuses the bare word Ok, one of the closest naming matches of any language in this comparison.Skipping the failure case is a compile error
Divide = fun(_, 0) -> {error, divide_by_zero};
(X, Y) -> {ok, X div Y}
end,
% Nothing stops you from assuming success — this crashes with a
% badmatch if Divide returns {error, _} instead of {ok, _}:
try
{ok, Value} = Divide(10, 0),
io:format("~p~n", [Value])
catch
error:{badmatch, _} -> io:format("crashed: badmatch~n")
end. let divide x y =
if y = 0 then Error "divide by zero"
else Ok (x / y)
let () =
match divide 10 0 with
| Ok value -> Printf.printf "%d\n" value
| Error reason -> Printf.printf "Error: %s\n" reason Nothing in Erlang forces you to handle the failure branch — skipping it just defers the crash until a bad input eventually reaches that assumption. Treating an OCaml
(int, string) result as if it were a bare int is a type error the compiler catches immediately; matching both Ok and Error is the idiomatic way to satisfy it.Lists
Lists and the cons pattern
[Head | Tail] = [1, 2, 3],
io:format("~p and ~p~n", [Head, Tail]). let (head :: tail) = [1; 2; 3]
let () = Printf.printf "%d and (list)\n" head Both languages model lists as singly-linked cons cells. Erlang spells the pattern
[Head | Tail]; OCaml spells it head :: tail, the same :: cons operator OCaml lists are built from. OCaml lists must be homogeneous (one element type throughout); Erlang lists happily mix atoms, numbers, and tuples.Map and filter
Doubled = lists:map(fun(N) -> N * 2 end, [1, 2, 3, 4, 5]),
Evens = lists:filter(fun(N) -> N rem 2 =:= 0 end, Doubled),
io:format("~p~n", [Evens]). let doubled = List.map (fun n -> n * 2) [1; 2; 3; 4; 5]
let evens = List.filter (fun n -> n mod 2 = 0) doubled
let () = Printf.printf "%s\n" (String.concat "; " (List.map string_of_int evens)) Both languages call these
map and filter, with a near-identical argument order: (function, list) in both lists:map/2 and OCaml's List.map. This is one of the closer standard-library vocabulary matches across the whole comparison.Folding a list
Total = lists:foldl(fun(N, Accumulator) -> N + Accumulator end, 0, [1, 2, 3, 4, 5]),
io:format("~p~n", [Total]). let total = List.fold_left (fun accumulator n -> n + accumulator) 0 [1; 2; 3; 4; 5]
let () = Printf.printf "%d\n" total Erlang's
lists:foldl/3 and OCaml's List.fold_left take the same three pieces — function, initial accumulator, list — in the same order, another close standard-library match. Note the accumulator comes first inside the lambda in both, opposite the element.List Comprehensions: a Genuine Gap
No native comprehension syntax — a real gap versus Haskell
Squares = [X * X || X <- [1, 2, 3, 4, 5]],
io:format("~p~n", [Squares]). let squares = List.map (fun x -> x * x) [1; 2; 3; 4; 5]
let () = Printf.printf "%s\n" (String.concat "; " (List.map string_of_int squares)) This is a genuine, surprising gap for an Erlang developer used to
[Expr || Generator] comprehension syntax (which Erlang borrowed directly from Haskell). Standard OCaml has NO built-in list-comprehension syntax at all — the equivalent transformation is always spelled out with ordinary List.map/List.filter calls, unlike Erlang and Haskell, which both offer a dedicated comprehension notation for exactly this pattern.A filtered comprehension becomes a chained filter/map
Evens = [X || X <- lists:seq(1, 10), X rem 2 =:= 0],
io:format("~p~n", [Evens]). let evens = List.filter (fun x -> x mod 2 = 0) (List.init 10 (fun i -> i + 1))
let () = Printf.printf "%s\n" (String.concat "; " (List.map string_of_int evens)) Erlang appends the filter condition after a comma inside the same comprehension brackets, reading as one fluent expression. OCaml has to spell the same idea out as a separate
List.filter call composed with whatever generates the source list (here List.init, since OCaml has no lists:seq/2-style range literal either) — noticeably more verbose for what Erlang expresses in one bracketed expression.Recursion & Tail Calls
Factorial
Factorial = fun
Factorial(0) -> 1;
Factorial(N) when N > 0 -> N * Factorial(N - 1)
end,
io:format("~p~n", [Factorial(5)]). let rec factorial n =
match n with
| 0 -> 1
| n when n > 0 -> n * factorial (n - 1)
| _ -> invalid_arg "factorial"
let () = Printf.printf "%d\n" (factorial 5) A base-case clause and a recursive clause, in the same order — nearly a mechanical translation. The one new keyword: OCaml requires
rec to explicitly mark a function as recursive (let rec factorial), since ordinary let bindings in OCaml cannot refer to themselves — a small but real syntactic difference from Erlang, where every fun can always call itself by name.Tail recursion with an accumulator — both guarantee it
SumList = fun
SumList([], Accumulator) -> Accumulator;
SumList([Head | Tail], Accumulator) -> SumList(Tail, Head + Accumulator)
end,
io:format("~p~n", [SumList([1, 2, 3, 4, 5], 0)]). let rec sum_list numbers accumulator =
match numbers with
| [] -> accumulator
| head :: tail -> sum_list tail (head + accumulator)
let () = Printf.printf "%d\n" (sum_list [1; 2; 3; 4; 5] 0) The accumulator-passing style every Erlang developer already relies on transfers unchanged. OCaml, like Erlang's BEAM, guarantees tail-call optimization for calls in tail position, so this runs in constant stack space in both languages — one of the few guarantees that carries over exactly, not just in spirit.
Higher-Order Functions & Currying
Functions as values
Apply = fun(Function, Value) -> Function(Value) end,
Increment = fun(X) -> X + 1 end,
io:format("~p~n", [Apply(Increment, 41)]). let apply f value = f value
let increment x = x + 1
let () = Printf.printf "%d\n" (apply increment 41) Both languages treat functions as ordinary values, freely passed and stored. Notice OCaml's
apply needs no type annotations at all here — inference figures out that f must be a function from increment's usage, staying just as terse as the Erlang original.Every function is curried automatically — unlike Erlang
% Erlang funs have a fixed arity — building a partial application means
% wrapping it in a new fun by hand:
Add = fun(X, Y) -> X + Y end,
AddFive = fun(Y) -> Add(5, Y) end,
io:format("~p~n", [AddFive(10)]). let add x y = x + y
let add_five = add 5
let () = Printf.printf "%d\n" (add_five 10) Here the languages genuinely diverge. Erlang requires writing the wrapping
fun by hand to get a one-argument version of a two-argument function. In OCaml, add 5 alone is already valid — every function is secretly a chain of one-argument functions, so supplying fewer arguments than expected simply returns another function, with no wrapping required.Immutable-Only vs. Opt-in Mutation
A real capability gap: OCaml permits genuine mutation
% Erlang has no mutable variable at all — "changing" a value always
% means a new binding or a new recursive call argument:
Count = 0,
NewCount = Count + 1,
io:format("~p~n", [NewCount]). let count = ref 0
let () =
count := !count + 1;
Printf.printf "%d\n" !count This is a genuine capability gap running the opposite direction from most of this site's Erlang comparisons: OCaml's
ref cells and := assignment operator allow real, in-place mutation of a value shared across a whole program run — something no Erlang construct can do at all, since even Erlang process state is really "a new recursive call with a new argument" under the hood. OCaml requires opting into mutability explicitly per binding (via ref or a mutable record field); Erlang structurally cannot offer it in any form.Mutable record fields: another OCaml-only capability
% Again, no Erlang equivalent — a record field can never be
% reassigned in place; a "counter" record must always be rebuilt:
Counter = {counter, 0},
{counter, Value} = Counter,
NewCounter = {counter, Value + 1},
io:format("~p~n", [NewCounter]). type counter = { mutable tally : int }
let () =
let counter = { tally = 0 } in
counter.tally <- counter.tally + 1;
Printf.printf "%d\n" counter.tally OCaml records may declare individual fields
mutable, permitting <- assignment to update that field in place without rebuilding the whole record — Erlang has no equivalent whatsoever, since every Erlang tuple/record is entirely immutable and "updating" always means constructing a brand-new value.Flat Modules vs. a Real Module System
Erlang modules are flat; OCaml modules genuinely nest and parameterize
% Erlang modules are always one flat file, one namespace — there
% is no nesting, no parameterization, and no signature/interface
% concept separate from the exported function list:
io:format("~p~n", [erlang_modules_are_flat]). module Shapes = struct
type shape = Circle of float | Rectangle of float * float
let area = function
| Circle radius -> 3.14159 *. radius *. radius
| Rectangle (width, height) -> width *. height
end
let () = Printf.printf "%g\n" (Shapes.area (Shapes.Circle 5.0)) Erlang modules are a flat, one-file, one-namespace mechanism — export a function or don't, with no deeper structure available. OCaml's module system is a genuinely richer, separate layer of the language: modules can nest inside each other, be parameterized by other modules (functors), and be constrained by explicit signatures (
module type) independent of their implementation — a considerably more powerful toolkit with no direct Erlang equivalent.Exceptions: a Shared, Comfortable Tool
try/catch and try/with — both languages reach for exceptions readily
try
1/0
catch
error:badarith -> io:format("Caught division error~n")
end. let () =
try
let _ = 1 / 0 in
()
with
| Division_by_zero -> print_endline "Caught division error" Unlike Haskell (which prefers
Either/Maybe for nearly everything and treats exceptions as a last resort), both Erlang and OCaml reach for genuine exceptions comfortably and often — try ... catch and try ... with are both everyday, idiomatic tools in these languages, not an escape hatch reserved for truly exceptional situations.Declaring and raising a custom exception
% Erlang: throw a tagged tuple and catch it by pattern —
% there is no separate "exception declaration" step at all:
try
throw({invalid_age, -1})
catch
throw:{invalid_age, Age} -> io:format("Invalid age: ~p~n", [Age])
end. exception Invalid_age of int
let () =
try
raise (Invalid_age (-1))
with
| Invalid_age age -> Printf.printf "Invalid age: %d\n" age OCaml requires declaring an exception up front with the
exception keyword, giving it a genuine name and payload type the compiler checks. Erlang has no separate exception-declaration step at all — throw({invalid_age, -1}) just throws an ordinary tagged tuple, caught by pattern matching the same way any other value would be, with no nominal exception type involved.Gotchas for Erlang Developers
A "string" means something different again
% Erlang: "hello" is a LIST of character codes:
Value = "hello",
io:format("~w~n", [Value]). % shows [104,101,108,108,111] let value = "hello" (* a real, immutable byte sequence, not a list *)
let () = print_endline value Erlang's
"hello" is secretly [104, 101, 108, 108, 111], a plain list of integer code points — which is why ~w ("write the raw term") shows the numbers, while ~p/~s reprint it as text. OCaml's "hello" is a genuine, distinct string type — an immutable byte sequence, not a list of characters or code points at all, and not even iterable the way OCaml lists are without an explicit conversion.Both languages are strict — no Haskell-style surprises here
% Erlang evaluates eagerly, always — there is no laziness to
% reason about, no thunks, no infinite lists:
Value = 2 * 2,
io:format("~p~n", [Value]). let value = 2 * 2 (* computed immediately, right here — OCaml has
no laziness by default either, unlike Haskell *)
let () = Printf.printf "%d\n" value Unlike the Haskell comparisons elsewhere on this site, an Erlang developer moving to OCaml finds no laziness-related surprises at all — both languages are strict by default, evaluating every expression immediately in the order written, with no thunks and no infinite-list idiom to learn. (OCaml does offer an explicit opt-in
Lazy module for the rare cases that need deferred evaluation, matching Erlang's complete absence of anything similar.)