Skip to content

F# Guide — Immutable Data: Values That Never Change

DodaTech Updated 2026-06-28 5 min read

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