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.