On this page

Imperative Loops (for, while) — and Why OCaml Programmers Avoid Them by Default

Published on July 21, 2026

Enrico Hochkoeppler Profile

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​

Found this helpful? Share the knowledge...

... or support this project to keep it alive!

Buy Me a Coffee

Previous

Mutable State

Next

Reading Unfamiliar Type Signatures