- Courses
- 💻Programming Languages
- 🐫OCaml (WIP)
- Type Inference & Higher-Order Functions
- Imperative Loops: For, While
On this page
Imperative Loops (for, while) — and Why OCaml Programmers Avoid Them by Default
Published on July 21, 2026
By Enrico Hochkoeppler
Web Developer & TutoriaLibre Founder
OCaml has ordinary for and while loops, and they work exactly as expected. What's genuinely different from an imperative language is how rarely they show up in idiomatic OCaml code, in favor of recursion and higher-order functions. This isn't a stylistic preference enforced from outside the language — it comes directly from two concrete properties of loops in OCaml: they can never produce a value, and there's no built-in way to break out of one early.
for Loops
A for loop iterates a counter over an inclusive range, running its body once per value:
for i = 1 to 5 do
print_int i;
print_newline ()
done
The downto keyword counts backward instead:
for i = 5 downto 1 do
print_int i
done
while Loops
A while loop repeats its body for as long as a condition stays true, checking that condition before each iteration:
let i = ref 0 in
while !i < 5 do
print_int !i;
incr i
done
Since OCaml has no mutable variables, a while loop's condition can only change over time by mutating something explicitly — typically a ref, as above — which is why while loops and ref cells show up together constantly. Without some form of mutable state to update, a while loop's condition would never change, and the loop would simply never terminate.
The Core Limitation: Loops Always Produce unit
Both for and while are expressions, like everything else in OCaml, and both always evaluate to () — there is no way for either kind of loop to hand back a computed value directly. This is a hard, structural limitation, not a matter of style: a for loop's counter variable isn't even accessible outside the loop body, and neither loop construct has any syntax for returning something.
This becomes obvious the moment a loop needs to accumulate a result, such as summing the elements of a list. It can't be done with a loop alone — a ref has to carry the accumulating value on the loop's behalf, and the final answer has to be read back out of that ref once the loop finishes:
let sum_loop lst =
let total = ref 0 in
List.iter (fun x -> total := !total + x) lst;
!total
Compare this to the same computation written with a fold:
let sum_fold lst = List.fold_left ( + ) 0 lst
The fold-based version says directly what's happening — combine every element into a running total, starting from 0 — with the value itself flowing naturally from the computation. The loop-based version needs an extra mutable cell purely as a workaround for the fact that the loop itself can't return anything, plus an extra line to read the final value back out once the loop is done. Recursive functions and higher-order functions don't have this limitation at all: a recursive function returns whatever its final expression evaluates to, directly, with no auxiliary mutable state required just to get a value out.
The Second Limitation: No Way to Break Early
Neither for nor while has a break keyword. If a loop needs to stop iterating as soon as some condition is met partway through — searching a list and stopping at the first match, for instance — a plain loop has no direct way to express "stop now." The usual workaround is to raise an exception specifically to unwind out of the loop, then catch it immediately afterward:
exception Found of int
let find_first_negative arr =
try
for i = 0 to Array.length arr - 1 do
if arr.(i) < 0 then raise (Found i)
done;
None
with Found i -> Some i
This works, but it's a noticeably heavier piece of machinery than the problem seems to call for — defining a dedicated exception, wrapping the loop in a try, and raising purely to escape early, rather than to signal an actual error condition. A recursive function expresses the same "stop as soon as a condition is met" logic directly, using an ordinary match and no exception at all:
let rec find_first_negative_rec i arr =
if i >= Array.length arr then None
else if arr.(i) < 0 then Some i
else find_first_negative_rec (i + 1) arr
Here, returning Some i the moment a negative element is found is the early exit — there's no loop to break out of in the first place, since recursion simply stops calling itself once a base case is reached.
When a Loop Genuinely Is the Right Tool
None of this means loops are wrong to use — they're the clearer and sometimes more efficient choice in specific situations, mainly when working directly with an array (or other genuinely mutable structure) purely for its side effects, with no value that needs to be threaded back out of the iteration at all:
let fill_squares arr =
for i = 0 to Array.length arr - 1 do
arr.(i) <- i * i
done
Here, there's no accumulated result to hand back — the entire point of the loop is the in-place mutation of arr, which is exactly the situation a for loop was designed for, and a recursive rewrite of the same logic wouldn't be any clearer or safer. The general pattern worth internalizing: reach for a loop when a computation is fundamentally about repeatedly mutating something in place with no meaningful value to return; reach for recursion or a higher-order function once a value actually needs to come back out of the iteration, since that's exactly the situation where loops need a workaround and recursion doesn't.
Key Takeaways
forandwhileloops exist in OCaml and work as expected, but both always evaluate tounit— neither can hand back a computed value directly.- Accumulating a result across a loop requires an auxiliary
ref, read back out once the loop finishes; recursive and higher-order functions return their result directly, with no such workaround needed. - Neither loop has a
breakconstruct; exiting early requires raising and catching an exception purely for control flow, whereas a recursive function can simply stop calling itself. - Loops remain the clearer, sometimes more efficient choice specifically for in-place mutation of arrays or similar structures where no value needs to be threaded back out of the iteration.
- The deciding question is usually: does this iteration need to produce a value? If yes, recursion or a higher-order function is typically the more direct fit; if the entire point is mutating something in place with nothing to return, a loop is a reasonable and idiomatic choice.
Found this helpful? Share the knowledge...
... or support this project to keep it alive!
Previous
Mutable State
Next
Reading Unfamiliar Type Signatures
- for Loops
- while Loops
- The Core Limitation: Loops Always Produce unit
- The Second Limitation: No Way to Break Early
- When a Loop Genuinely Is the Right Tool
- Key Takeaways
