On this page

Mutable State: ref, Mutable Record Fields, Arrays

Published on July 21, 2026

Enrico Hochkoeppler Profile

By Enrico Hochkoeppler

Web Developer & TutoriaLibre Founder

OCaml has no mutable variables. Once a name is bound with let, what it refers to never changes — there's no assignment operator that reaches into a binding and replaces its value. And yet OCaml is not a purely functional language: it supports genuine, in-place mutation, just entirely through data structures rather than through variables themselves. There are exactly three built-in ways to do this: the ref cell, record fields explicitly marked mutable, and arrays, which are mutable by design.

ref: A Container for a Single Mutable Value​

A ref is a container holding exactly one value, which can be replaced in place at any time:


let counter = ref 0

# counter;;

- : int ref = {contents = 0}

Three operations work with a ref: ref v creates a new one containing v; !r reads its current contents; and r := v replaces its contents with v.


# !counter;;

- : int = 0

# counter := 5;;

- : unit = ()

# !counter;;

- : int = 5

There's no magic here at all — 'a ref is, quite literally, a record type with a single field named contents, marked mutable:


type 'a ref = { mutable contents : 'a }

ref, !, and := are themselves nothing more than ordinary functions built on top of that one-field record:


let ref v = { contents = v }

let ( ! ) r = r.contents

let ( := ) r v = r.contents <- v

This is worth internalizing early, because it means ref isn't a separate mutability mechanism bolted onto the language — it's simply the standard library's own record-with-a-mutable-field, wrapped in a small amount of convenient syntax.

Mutable Record Fields​

Any record field can be declared mutable, and once it is, its contents can be replaced in place using the <- operator, rather than needing a full functional update to produce a new record:


type point = { mutable x : float; mutable y : float }



let p = { x = 0.0; y = 0.0 }

let () = p.x <- 5.0

# p;;

- : point = {x = 5.0; y = 0.0}

A field left without the mutable keyword remains immutable, exactly as covered for ordinary records — a single record type is free to mix mutable and immutable fields side by side, mutating only the ones that were explicitly marked for it. One syntactic detail worth noting: <- is a language construct built directly into OCaml's grammar, not an ordinary function the way := is for refs — it has no type of its own and can't be passed around or partially applied the way a genuine operator function could.

Arrays: Mutable by Design​

Where a list is immutable and rebuilt whenever it needs to change, an array has a fixed length and supports true in-place mutation of any of its elements, using .(i) <- v:


let a = [| 1; 2; 3 |]

let () = a.(0) <- 99

# a;;

- : int array = [|99; 2; 3|]

Arrays give constant-time access and update by index, which lists fundamentally cannot, since reaching the n-th element of a list requires walking through the n cells before it. This is exactly why arrays exist as a separate, deliberately mutable data structure rather than OCaml simply making lists mutable instead: the two data structures are suited to genuinely different access patterns, and keeping lists immutable preserves the structural-sharing benefits that make immutable data cheap to work with in the first place.

Aliasing: The New Risk Mutation Introduces​

Mutable data introduces a genuinely new category of behavior that immutable data never has to worry about: two different names can refer to the very same mutable cell, so mutating through one name is visible through the other.


let a = ref 1

let b = a

let () = b := 2

# !a;;

- : int = 2

b was never assigned a copy of a's contents — b and a are two names for the exact same ref cell, so mutating through b is indistinguishable from mutating through a directly. This situation is called aliasing, and it's the reason OCaml distinguishes structural equality (=), which compares two values for having the same contents, from physical equality (==), which asks whether two values are, in fact, the very same underlying memory location:


# a = b;;

- : bool = true   (* same contents *)

# a == b;;

- : bool = true   (* actually the same cell *)

# a == ref 2;;

- : bool = false  (* same contents, but a freshly allocated, different cell *)

Aliasing doesn't come up at all with immutable values, since there's no way to observe whether two immutable values are "the same cell" or merely equal copies — nothing can ever change, so the distinction is invisible. It only becomes a genuine concern the moment mutation enters the picture, which is one of the main reasons immutable data is generally easier to reason about locally.

A Safe Pattern: Encapsulated Mutation​

Mutable state doesn't have to leak outward just because it's used internally. A function can freely use a ref and a loop on the inside, while still presenting a completely pure, side-effect-free interface to its caller:


let sum_array arr =

  let total = ref 0 in

  for i = 0 to Array.length arr - 1 do

    total := !total + arr.(i)

  done;

  !total

From the outside, sum_array behaves exactly like a pure function: calling it twice with the same array always produces the same result, and nothing about its mutable total is ever visible to, or reachable by, any caller. This is a genuinely useful and common pattern: use mutation freely inside a function's implementation when it's the clearer or more efficient tool, as long as none of that mutable state escapes across the function's boundary.

A Mutable Data Structure: A Linked List Node​

Mutable record fields make it possible to build data structures that are mutated in place rather than rebuilt, such as a mutable singly-linked list:


type 'a node = { value : 'a; mutable next : 'a node option }

Each node holds an immutable value and a next field that can be updated to point somewhere else after the node already exists — which is exactly what makes appending to, or splicing into, a structure like this possible without rebuilding the earlier nodes at all, unlike an ordinary immutable list, where any change to the tail rebuilds the cells leading up to it. This trade-off — in-place update at the cost of aliasing risk — is precisely what makes mutable data structures worth reaching for deliberately, rather than by default.

Key Takeaways​

Found this helpful? Share the knowledge...

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

Buy Me a Coffee

Previous

Currying & Partial Application

Next

Imperative Loops: For, While