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.
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.
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.
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 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.
// 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#.
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.
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.