F# Guide — Immutable Data: Values That Never Change
In this tutorial, you will learn about F# Guide. We cover key concepts, practical examples, and best practices to help you master this topic.
Immutable data means that once a value is bound to a name, it cannot be changed. F# enforces immutability by default, making programs easier to reason about, test, and parallelize.
What You'll Learn
- Immutable bindings with let
- Mutable variables with let mutable
- Working with immutable collections
- Copy-and-update expressions
- Thread Safety benefits
Why It Matters
Immutability eliminates entire categories of bugs: no unexpected mutations, no race conditions from shared state, and no defensive copies. Durga Antivirus Pro uses immutable data structures for thread-safe rule evaluation.
Real-World Use
Financial systems use immutable data for audit trails. Concurrent systems avoid locks by sharing immutable state. Functional code is naturally testable because functions only depend on inputs.
flowchart LR
A["Immutable Data"] --> B["Bindings"]
B --> C["Collections"]
C --> D["Copy & Update"]
D --> E["Mutability"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Immutable Bindings
// let creates an immutable binding
let x = 5
// x <- 10 // Error: x is immutable
// Shadowing creates a new binding
let x = 5
let x = x + 1 // New x, not mutation
Mutable Variables
// Explicit mutability
let mutable counter = 0
counter <- counter + 1 // OK
// Mutable is useful for:
// - Accumulators in loops
// - Performance-critical code
// - Interop with .NET libraries
Immutable Collections
// Lists are immutable
let list1 = [1; 2; 3]
let list2 = 0 :: list1 // [0; 1; 2; 3] - new list
// list1 is still [1; 2; 3]
// Arrays are mutable by default
let arr = [|1; 2; 3|]
arr.[0] <- 99 // OK - arrays are mutable
// Sequences are immutable
let seq1 = seq { 1..10 }
Copy-and-Update
type Person = { Name: string; Age: int }
let alice = { Name = "Alice"; Age = 30 }
// Copy with modifications
let olderAlice = { alice with Age = 31 }
// alice is still { Name = "Alice"; Age = 30 }
// Nested records
type Address = { Street: string; City: string }
type Employee = { Name: string; Address: Address }
let emp = { Name = "Bob"; Address = { Street = "123 Main"; City = "NYC" } }
let moved = { emp with Address = { emp.Address with City = "LA" } }
Why Immutability Matters
// Thread safety - no locks needed
let sharedData = [1; 2; 3; 4; 5]
// Multiple threads can read sharedData
// No thread can modify it, so no race conditions
// Predictable code
let process x =
let temp = x * 2
let temp = temp + 1
temp
// Each binding is a clear step
Working with Immutable Data
// List operations return new lists
let numbers = [1..5]
let doubled = numbers |> List.map (fun x -> x * 2)
let even = numbers |> List.filter (fun x -> x % 2 = 0)
let sum = numbers |> List.sum
// Set operations
let set1 = Set.ofList [1; 2; 3]
let set2 = Set.ofList [3; 4; 5]
let union = Set.union set1 set2
let intersect = Set.intersect set1 set2
Performance Considerations
// Immutable collections share structure
// List cons (::) reuses the tail
let a = [1; 2; 3]
let b = 0 :: a
// b = [0; 1; 2; 3], a = [1; 2; 3]
// The tail [1; 2; 3] is shared
// For mutable performance needs:
// - Use arrays for indexed access
// - Use mutable locals in tight loops
// - Use ResizeArray for dynamic growth
Common Mistakes
1. Trying to mutate let bindings
let x = 5; x <- 6 is an error. Use let mutable or create a new binding.
2. Confusing shadowing with mutation
Shadowing creates a new value with the same name. The original is unchanged. This is not mutation.
3. Expecting list modifications to affect the original
List.append returns a new list. The original is unchanged. Always use the return value.
4. Overusing mutable when immutable suffices
Prefer immutable by default. Only use mutable when profiling shows a performance need.
5. Forgetting that arrays are mutable
Arrays are .NET arrays underneath and are mutable. Use lists or sequences for immutability.
Practice Questions
1. What does immutable mean in F#? Once a value is bound to a name, it cannot be changed. Any "change" creates a new value.
2. How do you create a mutable variable?
Use let mutable name = value and update with <-.
3. What is the copy-and-update expression?
{ record with field = newValue } creates a new record instance with specified field changes.
Challenge: Write a function that processes a list of numbers without using any mutable variables, returning a new list with each number doubled.
FAQ
{{< faq question="Is immutability slower?" >}} Not necessarily. Immutable collections share structure, reducing copying costs. For most code, the safety benefits far outweigh any performance difference. {{< /faq >}}
{{< faq question="When should I use mutable?" >}} Use mutable for performance-critical inner loops, accumulating counters, and interop with .NET libraries that expect mutable state. {{< /faq >}}
{{< faq question="Are strings mutable in F#?" >}} No. .NET strings are immutable. Any operation that "changes" a string returns a new string. {{< /faq >}}
{{< faq question="How does immutability help with concurrency?" >}} Immutable data can be safely shared between threads without locks. No thread can modify shared state, eliminating race conditions. {{< /faq >}}
{{< faq question="What is structural sharing?" >}} Immutable collections reuse parts of existing structures when creating new ones. For example, adding to the front of a list reuses the original list as the tail. {{< /faq >}}
Mini Project
Build a simple bank account system with immutable transactions:
type Transaction = { Amount: decimal; Description: string }
type Account = { Balance: decimal; Transactions: Transaction list }
let applyTransaction account txn =
{ Balance = account.Balance + txn.Amount
Transactions = txn :: account.Transactions }
let account = { Balance = 1000m; Transactions = [] }
let txn1 = { Amount = -50m; Description = "Groceries" }
let txn2 = { Amount = 200m; Description = "Salary" }
let updated = account |> applyTransaction txn1 |> applyTransaction txn2
What's Next
Now that you understand immutable data, explore pattern matching for elegant conditional logic.
| Topic | Description | Link |
|---|---|---|
| F# Pattern Matching | Pattern matching basics | {{< ref "06-pattern-matching" >}} |
| F# Lists | List operations | {{< ref "07-lists" >}} |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro