F# reference cells
last modified May 3, 2025
In this article, we explore reference cells in F#. Reference cells provide a way to handle mutable state while maintaining functional programming principles.
A reference cell is a special container that holds a mutable value.
Unlike regular mutable variables, reference cells are first-class values that
can be passed around and stored in data structures. They are created with the
ref
function, accessed with !
, and modified with
:=
. Reference cells are particularly useful when you need mutable
state in otherwise immutable functional code.
F# reference cell basic example
This example shows the basic operations with reference cells.
let counter = ref 0 printfn "Initial value: %d" !counter counter := !counter + 1 printfn "After increment: %d" !counter counter := !counter + 1 printfn "After second increment: %d" !counter
We create a reference cell, then modify and access its contents.
let counter = ref 0
Creates a reference cell initialized with value 0. The ref function wraps the value in a mutable container.
!counter
The ! operator dereferences the cell to access its contained value.
counter := !counter + 1
The := operator updates the reference cell's value. We must first dereference the current value before incrementing it.
λ dotnet fsi basic.fsx Initial value: 0 After increment: 1 After second increment: 2
F# reference cell type annotation
Reference cells can have explicit type annotations for their contained values.
let message: string ref = ref "hello" let count: int ref = ref 0 let active: bool ref = ref true printfn "Message: %s" !message printfn "Count: %d" !count printfn "Active: %b" !active message := "world" count := 42 active := false printfn "After modification:" printfn "Message: %s" !message printfn "Count: %d" !count printfn "Active: %b" !active
We create three typed reference cells, print their initial values, modify them, and print again.
let message: string ref = ref "hello"
The type annotation string ref specifies this reference cell holds a string.
λ dotnet fsi typed.fsx Message: hello Count: 0 Active: true After modification: Message: world Count: 42 Active: false
F# reference cell in functions
Reference cells can be passed to and returned from functions.
let increment (cell: int ref) = cell := !cell + 1 let createCounter initialValue = ref initialValue let counter = createCounter 10 printfn "Initial: %d" !counter increment counter printfn "After increment: %d" !counter increment counter printfn "After second increment: %d" !counter
We demonstrate using reference cells with functions by creating a counter factory and an increment function.
let increment (cell: int ref) = cell := !cell + 1
A function that takes a reference cell and modifies its contents.
let createCounter initialValue = ref initialValue
A function that creates and returns a new reference cell.
λ dotnet fsi functions.fsx Initial: 10 After increment: 11 After second increment: 12
F# reference cell vs mutable variable
This example compares reference cells with regular mutable variables.
let mutable mvar = 0 let rvar = ref 0 printfn "mutable: %d, reference: %d" mvar !rvar mvar <- mvar + 1 rvar := !rvar + 1 printfn "After increment:" printfn "mutable: %d, reference: %d" mvar !rvar let modifyMutable x = x + 1 let modifyReference (x: int ref) = x := !x + 1 mvar <- modifyMutable mvar modifyReference rvar printfn "After function calls:" printfn "mutable: %d, reference: %d" mvar !rvar
We show the key differences between mutable variables and reference cells.
let modifyMutable x = x + 1 let modifyReference (x: int ref) = x := !x + 1
Reference cells can be modified inside functions, while mutable variables require the mutable value to be returned.
λ dotnet fsi comparison.fsx mutable: 0, reference: 0 After increment: mutable: 1, reference: 1 After function calls: mutable: 2, reference: 2
F# reference cell in data structures
Reference cells can be stored in lists and other data structures.
let counters = [ref 1; ref 2; ref 3; ref 4; ref 5] printfn "Initial values:" counters |> List.iter (fun x -> printf "%d " !x) printfn "" counters |> List.iter (fun x -> x := !x * 2) printfn "After modification:" counters |> List.iter (fun x -> printf "%d " !x) printfn ""
We create a list of reference cells, print their initial values, modify them, and print again.
let counters = [ref 1; ref 2; ref 3; ref 4; ref 5]
Creates a list where each element is a separate reference cell.
counters |> List.iter (fun x -> x := !x * 2)
Doubles the value in each reference cell in the list.
λ dotnet fsi structures.fsx Initial values: 1 2 3 4 5 After modification: 2 4 6 8 10
F# reference cell with records
Reference cells can be used within record types for mutable fields.
type Counter = { Name: string Count: int ref } let createCounter name initialValue = { Name = name Count = ref initialValue } let incrementCounter counter = counter.Count := !counter.Count + 1 printfn "%s: %d" counter.Name !counter.Count let c1 = createCounter "First" 0 let c2 = createCounter "Second" 10 incrementCounter c1 incrementCounter c2 incrementCounter c1 incrementCounter c2
We create a record type with a reference cell field and functions to work with it.
type Counter = { Name: string Count: int ref }
The Count field is a reference cell allowing mutation while the record remains immutable.
λ dotnet fsi records.fsx First: 1 Second: 11 First: 2 Second: 12
F# reference cell in closures
Reference cells maintain their state across function calls in closures.
let makeCounter() = let count = ref 0 fun () -> count := !count + 1 !count let counter1 = makeCounter() let counter2 = makeCounter() printfn "Counter1: %d" (counter1()) printfn "Counter1: %d" (counter1()) printfn "Counter2: %d" (counter2()) printfn "Counter1: %d" (counter1()) printfn "Counter2: %d" (counter2())
We demonstrate stateful closures using reference cells to create counters.
let makeCounter() = let count = ref 0 fun () -> count := !count + 1 !count
Each call to makeCounter creates a new reference cell captured in the closure.
λ dotnet fsi closures.fsx Counter1: 1 Counter1: 2 Counter2: 1 Counter1: 3 Counter2: 2
F# reference cell limitations
This example shows some limitations and considerations when using reference cells.
let cell = ref "hello" // This works cell := "world" printfn "%s" !cell // This would cause a compilation error // cell := 42 // Type mismatch // Need to dereference to use the value let length = String.length !cell printfn "Length: %d" length // Reference cells can be compared by reference let cell2 = ref "world" printfn "Same contents: %b" (!cell = !cell2) printfn "Same reference: %b" (cell = cell2)
We demonstrate type safety, dereferencing needs, and reference comparison.
// cell := 42 // Type mismatch
Reference cells are type-safe - you can't change the type of the contained value.
printfn "Same reference: %b" (cell = cell2)
Reference cells are compared by reference, not by their contents.
λ dotnet fsi limitations.fsx world Length: 5 Same contents: true Same reference: false
In this article we have explored reference cells in F#. They provide a powerful way to handle mutable state while maintaining functional programming principles.