Beginner

Control Flow in OCaml

Learn control flow in OCaml - if/else expressions, pattern matching, recursion, and imperative loops with Docker-ready examples

Control flow determines the order in which your program’s logic executes. OCaml is a multi-paradigm language, so it gives you two distinct styles: the functional approach built on expressions, pattern matching, and recursion, and a pragmatic imperative layer with for and while loops for when mutation is the clearest tool.

The first thing to internalize is that OCaml’s if/then/else is an expression, not a statement. It evaluates to a value, so you can bind its result to a name or use it directly as a function argument. This is fundamentally different from C-style languages where if is a control statement that does something but produces nothing.

The real star of OCaml control flow is pattern matching with the match expression. It replaces the switch/case of imperative languages but goes much further — it can destructure data, bind variables, and the compiler will warn you when you forget a case. Combined with recursion (which replaces most explicit loops in idiomatic code), pattern matching is how experienced OCaml programmers express branching logic.

In this tutorial you’ll learn how to write conditionals as expressions, match on values and data structures, use recursion to iterate, and reach for imperative loops when they fit. Because OCaml is statically typed and strongly inferred, the compiler checks that every branch of a conditional or match returns a compatible type — a powerful safety net you’ll see throughout.

Conditionals Are Expressions

In OCaml, if/else returns a value. Both branches must have the same type, and an if without an else implicitly returns unit (so its single branch must also be unit).

Create a file named conditionals.ml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
(* if/else is an EXPRESSION in OCaml - it evaluates to a value *)

let () =
  let temperature = 18 in

  (* Used for side effects: both branches return unit *)
  if temperature > 25 then
    print_endline "It's warm outside"
  else
    print_endline "Bring a jacket";

  (* Used as an expression: the result is bound to a name *)
  let label = if temperature > 20 then "warm" else "cool" in
  Printf.printf "The weather is %s\n" label;

  (* Chained conditions with else if *)
  let category =
    if temperature < 0 then "freezing"
    else if temperature < 15 then "cold"
    else if temperature < 25 then "mild"
    else "hot"
  in
  Printf.printf "Category: %s\n" category;

  (* An if with no else returns unit, so the branch must be unit too *)
  if temperature < 20 then print_endline "Below 20 degrees"

Because if is an expression, OCaml has no need for a separate ternary operator — if cond then a else b already plays that role.

Pattern Matching: The match Expression

match is OCaml’s most powerful control-flow construct. It tests a value against a series of patterns, runs the first that matches, and can bind variables, deconstruct data, and apply guards with when.

Create a file named matching.ml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
(* Match on plain integer values; _ is the catch-all wildcard *)
let describe_digit n =
  match n with
  | 0 -> "zero"
  | 1 -> "one"
  | 2 -> "two"
  | _ -> "many"

(* Guards (when clauses) add conditions to a pattern *)
let classify n =
  match n with
  | 0 -> "zero"
  | n when n < 0 -> "negative"
  | n when n mod 2 = 0 -> "positive even"
  | _ -> "positive odd"

(* Match on a variant type - the compiler warns if a case is missing *)
type traffic_light = Red | Yellow | Green

let action light =
  match light with
  | Red -> "stop"
  | Yellow -> "slow down"
  | Green -> "go"

(* Match deconstructs lists into head and tail *)
let describe_list lst =
  match lst with
  | [] -> "empty"
  | [x] -> Printf.sprintf "one element: %d" x
  | x :: _ -> Printf.sprintf "starts with %d" x

let () =
  print_endline (describe_digit 2);
  print_endline (classify (-5));
  print_endline (classify 8);
  print_endline (action Green);
  print_endline (describe_list [42; 7; 9])

The x :: _ pattern splits a list into its first element (x) and the rest (ignored with _). This destructuring is what makes recursion over lists so natural in OCaml.

Recursion Replaces Loops

In idiomatic functional OCaml, recursion is the primary way to iterate. A function marked rec can call itself, and tail-recursive functions (where the recursive call is the last thing done) run in constant stack space — just like a loop.

Create a file named recursion.ml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
(* Count down from n using recursion; begin...end groups expressions *)
let rec countdown n =
  if n <= 0 then
    print_endline "Liftoff!"
  else begin
    Printf.printf "%d... " n;
    countdown (n - 1)
  end

(* Classic (non-tail) recursion: factorial *)
let rec factorial n =
  if n <= 1 then 1
  else n * factorial (n - 1)

(* Tail-recursive sum using an accumulator - runs in constant stack space *)
let sum_to n =
  let rec loop acc i =
    if i > n then acc
    else loop (acc + i) (i + 1)
  in
  loop 0 1

(* Walk a list with the function keyword (shorthand for match) *)
let rec sum_list = function
  | [] -> 0
  | x :: rest -> x + sum_list rest

let () =
  countdown 3;
  Printf.printf "factorial 5 = %d\n" (factorial 5);
  Printf.printf "sum 1..100 = %d\n" (sum_to 100);
  Printf.printf "sum of list = %d\n" (sum_list [10; 20; 30])

The function keyword is shorthand for a fun that immediately pattern-matches its argument — a very common idiom for recursive list processing.

Imperative Loops When You Need Them

OCaml is pragmatic: when mutation is the clearest approach, it offers for and while loops. These work alongside mutable references (ref) and arrays. OCaml has no break statement, so early exit from a loop is done by raising an exception.

Create a file named loops.ml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let () =
  (* A for loop counting up with "to" *)
  print_string "Counting up:";
  for i = 1 to 5 do
    Printf.printf " %d" i
  done;
  print_newline ();

  (* A for loop counting down with "downto" *)
  print_string "Counting down:";
  for i = 5 downto 1 do
    Printf.printf " %d" i
  done;
  print_newline ();

  (* A while loop driven by a mutable reference *)
  let n = ref 16 in
  print_string "Halving:";
  while !n > 0 do
    Printf.printf " %d" !n;
    n := !n / 2
  done;
  print_newline ();

  (* No "break" in OCaml - raise a local exception for early exit *)
  let exception Found of int in
  let numbers = [| 4; 8; 15; 16; 23; 42 |] in
  try
    for i = 0 to Array.length numbers - 1 do
      if numbers.(i) > 20 then raise (Found numbers.(i))
    done;
    print_endline "No value over 20 found"
  with Found v -> Printf.printf "First value over 20: %d\n" v

Note the !n syntax: ! dereferences a ref (reads its current value), while := assigns a new one. The local exception Found lets us jump out of the for loop the moment we find a match.

Running with Docker

Run each example with the official OCaml image. The ocaml command interprets the file directly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official OCaml image
docker pull ocaml/opam:alpine

# Run the conditionals example
docker run --rm -v $(pwd):/home/opam/app -w /home/opam/app ocaml/opam:alpine ocaml conditionals.ml

# Run the pattern matching example
docker run --rm -v $(pwd):/home/opam/app -w /home/opam/app ocaml/opam:alpine ocaml matching.ml

# Run the recursion example
docker run --rm -v $(pwd):/home/opam/app -w /home/opam/app ocaml/opam:alpine ocaml recursion.ml

# Run the imperative loops example
docker run --rm -v $(pwd):/home/opam/app -w /home/opam/app ocaml/opam:alpine ocaml loops.ml

Expected Output

conditionals.ml:

Bring a jacket
The weather is cool
Category: mild
Below 20 degrees

matching.ml:

two
negative
positive even
go
starts with 42

recursion.ml:

3... 2... 1... Liftoff!
factorial 5 = 120
sum 1..100 = 5050
sum of list = 60

loops.ml:

Counting up: 1 2 3 4 5
Counting down: 5 4 3 2 1
Halving: 16 8 4 2 1
First value over 20: 23

Key Concepts

  • if/else is an expression — it evaluates to a value, so both branches must share the same type. An if with no else must return unit. This removes the need for a separate ternary operator.
  • match is the workhorse of control flow — it compares a value against patterns, binds variables, and destructures data structures like lists and variants in one construct.
  • Guards (when) attach extra boolean conditions to a pattern, letting you branch on computed properties such as n mod 2 = 0.
  • Exhaustiveness checking — the compiler warns when a match over a variant type misses a case, catching whole classes of bugs before the program runs.
  • Recursion replaces loops in idiomatic OCaml; tail-recursive functions with an accumulator run in constant stack space, just like an imperative loop.
  • The function keyword is shorthand for a one-argument match, ideal for recursive list processing.
  • Imperative for and while loops are available for side-effecting code, working with ref cells (! reads, := assigns) and mutable arrays.
  • No break statement — raise an exception (often a local let exception ... in) to exit a loop early.

Running Today

All examples can be run using Docker:

docker pull ocaml/opam:alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining