ZetCode

F# mutability

last modified May 17, 2025

In this article we cover mutability in F#. F# is a functional-first language that emphasizes immutability. However, there are scenarios where mutable data is necessary.

While immutability is the default approach in F#, the language provides structured mechanisms to handle mutable data when necessary. By understanding how and when to use mutability effectively, developers can balance functional programming principles with real-world application needs, ensuring optimal performance and clarity in their code.

Mutability in F# can be managed using various constructs, such as mutable variables, reference cells (ref), and mutable records or classes. Each approach serves different needs, whether for local state modification, encapsulated object mutability, or controlled updates in larger applications. Choosing the right mutability mechanism depends on the specific problem being solved while maintaining code safety and readability.

Mutable variables

The mutable keyword allows creating variables that can be modified after declaration. Use the <- operator to assign new values.

mutable_vars.fsx
let mutable counter = 0
printfn "Initial counter: %d" counter

counter <- counter + 1
printfn "Updated counter: %d" counter

// Mutable variable in a loop
for i in 1..5 do
    counter <- counter + i
    printfn "Loop iteration %d: %d" i counter

// Type inference works with mutable variables
let mutable message = "Hello"
message <- message + ", there!"
printfn "%s" message

This example shows basic mutable variable usage. Note that the scope of mutability is limited to the variable declaration.

λ dotnet fsi mutable_vars.fsx
Initial counter: 0
Updated counter: 1
Loop iteration 1: 2
Loop iteration 2: 4
Loop iteration 3: 7
Loop iteration 4: 11
Loop iteration 5: 16
Hello, there!

Reference cells

Reference cells (ref) provide another way to handle mutable state, wrapping values in a container that can be updated.

reference_cells.fsx
let counterRef = ref 0
printfn "Initial counter: %d" counterRef.Value

counterRef.Value <- counterRef.Value + 1
printfn "Updated counter: %d" counterRef.Value

// Reference cells in functions
let increment (refCell: int ref) =
    refCell.Value <- refCell.Value + 1

increment counterRef
printfn "After increment: %d" counterRef.Value

// Using ref cells with closures
let createCounter() =
    let count = ref 0
    fun () -> count.Value <- count.Value + 1; count.Value

let counter = createCounter()
printfn "Counter calls: %d %d %d" (counter()) (counter()) (counter())

In the example, we create a reference cell to hold a mutable integer. The increment function modifies the value inside the reference cell. We also demonstrate how to create a closure that maintains its own mutable state using a reference cell. The Value property is used to access and modify the value inside the reference cell.

λ dotnet fsi reference_cells.fsx
Initial counter: 0
Updated counter: 1
After increment: 2
Counter calls: 1 2 3

Mutable records

Record fields can be marked as mutable, allowing selective mutability within immutable record types.

mutable_records.fsx
type Person = {
    Name: string
    mutable Age: int
    mutable Email: string
}

let person = { Name = "Alice"; Age = 30; Email = "alice@example.com" }
printfn "Original: %A" person

person.Age <- person.Age + 1
person.Email <- "new.email@example.com"
printfn "Updated: %A" person

// Mutable records in functions
let birthday p = 
    p.Age <- p.Age + 1
    p

let olderPerson = birthday person
printfn "After birthday: %A" olderPerson

Only fields marked as mutable can be modified. The record instance itself remains immutable.

λ dotnet fsi mutable_records.fsx
Original: { Name = "Alice"
  Age = 30
  Email = "alice@example.com" }
Updated: { Name = "Alice"
  Age = 31
  Email = "new.email@example.com" }
After birthday: { Name = "Alice"
  Age = 32
  Email = "new.email@example.com" }

Arrays and mutable collections

Arrays are inherently mutable in F#, and some collection types provide mutable versions. For instance, ResizeArray is a mutable list-like collection, and Dictionary is a mutable key-value store.

mutable_collections.fsx
// Mutable arrays
let numbers = [|1; 2; 3; 4|]
printfn "Original array: %A" numbers

numbers[1] <- 20
printfn "Modified array: %A" numbers

// ResizeArray (mutable List)
let names = ResizeArray<string>()
names.Add("Alice")
names.Add("Bob")
printfn "Names: %A" names

names[0] <- "Carol"
names.RemoveAt(1)
printfn "Updated names: %A" names

// Dictionary (mutable key-value store)
let inventory = System.Collections.Generic.Dictionary<string, int>()
inventory.Add("Apples", 10)
inventory.Add("Oranges", 5)

inventory["Apples"] <- 8
inventory["Bananas"] <- 3

printfn "Inventory:"
for item in inventory do
    printfn "- %s: %d" item.Key item.Value

These collections provide mutability while maintaining type safety.

λ dotnet fsi mutable_collections.fsx
Original array: [|1; 2; 3; 4|]
Modified array: [|1; 20; 3; 4|]
Names: seq ["Alice"; "Bob"]
Updated names: seq ["Carol"]
Inventory:
- Apples: 8
- Oranges: 5
- Bananas: 3

When to use mutability

Mutability has its place in scenarios where performance, efficiency, or compatibility with external APIs requires state changes. Using mutability strategically ensures that code remains clear and maintainable while benefiting from controlled state modifications.

when_to_use.fsx
// 1. Performance-critical code
let sumNumbers n =
    let mutable total = 0
    for i in 1..n do
        total <- total + i
    total

printfn "Sum of 1-100: %d" (sumNumbers 100)

// 2. Interoperability with .NET APIs
let sb = System.Text.StringBuilder()
sb.Append("Hello") |> ignore
sb.Append(", there!") |> ignore
printfn "%s" (sb.ToString())

// 3. Building collections incrementally
let generateSquares n =
    let squares = ResizeArray<int>()
    for i in 1..n do
        squares.Add(i * i)
    squares.ToArray()

printfn "Squares: %A" (generateSquares 5)

// 4. State in UI or game development
type GameState = {
    mutable Score: int
    mutable Level: int
}

let state = { Score = 0; Level = 1 }
state.Score <- state.Score + 100
printfn "Game state: %A" state

Mutability is particularly useful in performance-critical applications, where frequent updates to variables without excessive allocations are necessary. It is also essential for interoperability with .NET APIs, as many built-in .NET classes, such as StringBuilder, rely on mutable operations. Additionally, incrementally building collections using mutable lists or arrays can be more efficient than recursive immutable approaches.

Finally, mutability is often required in UI frameworks and game development, where state changes dynamically in response to user actions or gameplay mechanics. By understanding when and how to apply mutability effectively, developers can balance functional programming principles with practical demands for efficient state management.

λ dotnet fsi when_to_use.fsx
Sum of 1-100: 5050
Hello, there!
Squares: [|1; 4; 9; 16; 25|]
Game state: { Score = 100
  Level = 1 }

Best practices

Follow these guidelines when working with mutable data in F#.

best_practices.fsx
type Item = { Price: float }
    
// 1. Limit scope of mutability
let calculateTotal (items: Item list) =
    let mutable total = 0.0
    for item in items do
        total <- total + item.Price
    total // Immutable return

// 2. Prefer immutable by default
let immutableApproach items =
    items |> List.sumBy (fun item -> item.Price)

// 3. Isolate mutable state
type Counter() =
    let mutable count = 0
    member _.Next() =
        count <- count + 1
        count

let counter = Counter()
printfn "Counter: %d %d" (counter.Next()) (counter.Next())

let data = [ { Price = 10.0 }; { Price = 20.0 } ]
let total = calculateTotal data
printfn "Total: %f" total

let immutableTotal = immutableApproach data
printfn "Immutable Total: %f" immutableTotal

In the example, we demonstrate best practices for using mutability in F#. We limit the scope of mutability to a specific function, prefer immutable approaches when possible, and isolate mutable state within a class. This approach ensures that mutable state is well-defined and controlled, reducing the risk of unintended side effects.

Thread safety with mutability

Managing mutable state in concurrent scenarios requires careful synchronization to prevent race conditions and ensure data integrity. When multiple threads modify shared variables simultaneously, inconsistencies can arise, leading to unpredictable behavior. F# provides mechanisms such as locks, reference cells, and immutable data structures to help manage concurrent state effectively.

thread_safety.fsx
open System.Threading

// Unsafe mutable access
let mutable unsafeCounter = 0

let incrementUnsafe() =
    for _ in 1..100000 do
        unsafeCounter <- unsafeCounter + 1

// Thread-safe counter using `Value`
let safeCounter = ref 0
let lockObj = obj()

let incrementSafe() =
    for _ in 1..100000 do
        lock lockObj (fun () -> 
            safeCounter.Value <- safeCounter.Value + 1)

let t1 = Thread(incrementUnsafe)
let t2 = Thread(incrementUnsafe)

t1.Start()
t2.Start()
t1.Join()
t2.Join()

printfn "Unsafe counter: %d" unsafeCounter

let t3 = Thread(incrementSafe)
let t4 = Thread(incrementSafe)

safeCounter.Value <- 0  // Using `Value` for assignment
t3.Start()
t4.Start()
t3.Join()
t4.Join()

printfn "Safe counter: %d" safeCounter.Value  // Using `Value` for retrieval

The example below illustrates the difference between unsafe mutable access, where modifications happen without synchronization, and thread-safe updates, where a lock ensures atomic operations. The unsafe approach may lead to incorrect results due to race conditions, whereas the thread-safe approach guarantees proper value updates across multiple threads. Using synchronization techniques like locks minimizes risks associated with concurrent mutations while preserving performance and reliability.

λ dotnet fsi thread_safety.fsx
Unsafe counter: 117532
Safe counter: 200000

F# provides several controlled ways to work with mutable state when needed, while encouraging immutability by default. By understanding mutable variables, reference cells, mutable records, and collections, you can make informed decisions about when mutability is appropriate. Always prefer immutable solutions where possible and carefully manage mutable state when required.

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.