ZetCode

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.

basic.fsx
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.

typed.fsx
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.

functions.fsx
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.

comparison.fsx
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.

structures.fsx
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.

records.fsx
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.

closures.fsx
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.

limitations.fsx
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.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.