F# Copy by Value vs Reference
last modified May 16, 2025
This tutorial explains how F# handles value and reference types with its functional-first approach, emphasizing immutability and clear copy semantics. Understanding these concepts is crucial for writing correct F# code.
Immutability by Default
F# encourages immutability by default, which simplifies reasoning about code:
- Immutable values: Default binding with
letcreates immutable values - Mutable variables: Must be explicitly declared with
mutablekeyword - Value types: Primitive types, structs, and tuples
- Reference types: Classes, arrays, records (though records are immutable by default)
F# distinguishes between immutable and mutable types. Immutable types are copied by value, while mutable types are copied by reference. This means that assigning a mutable type creates a reference to the original object, not a logical copy.
| Characteristic | Immutable Types | Mutable Types |
|---|---|---|
| Default Behavior | Yes | No (requires mutable) |
| Copy Semantics | Copy by value (logical) | Reference semantics |
| Examples | int, string, records |
mutable vars, classes, arrays |
In the table above, we summarize the key differences between immutable and mutable types in F#. Immutable types are copied by value, while mutable types are copied by reference.
Immutable Value Types
F# treats primitive types as immutable values. Assignments create logical copies:
// Primitive types are immutable let a = 10 let b = a // Logical copy printfn "Original: a = %d, b = %d" a b let b' = b + 5 // Creates new value printfn "After change: a = %d, b' = %d" a b' // Tuples are immutable let tuple1 = (1, "hello") let tuple2 = tuple1 // Copy // tuple2.Item1 <- 2 // Would cause error
In the example above, a and b are both
immutable integers. The assignment b = a creates a logical
copy of a. When we modify b, it does not affect
a. The same applies to tuples, which are also immutable.
$ dotnet fsi Program.fs Original: a = 10, b = 10 After change: a = 10, b' = 15
Records and Discriminated Unions
F#'s record and discriminated union types are immutable by default. They allow for logical copying and updating.
type Person = { Name: string; Age: int }
type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
let person1 = { Name = "Alice"; Age = 30 }
let person2 = person1 // Copy
let person3 = { person2 with Age = 31 } // Copy with update
printfn "person1: %A" person1
printfn "person3: %A" person3
let shape1 = Circle 5.0
let shape2 = shape1 // Copy
In the example above, we define a record type Person and a
discriminated union type Shape. The assignment
person2 = person1 creates a logical copy of person1.
The with syntax allows us to create a new record with an updated
Age field while keeping the rest of the fields unchanged.
Mutable Variables
F# allows mutable variables when explicitly requested. The mutable
keyword is used to declare mutable variables. However, it is recommended to
use immutability by default and only use mutability when necessary.
let mutable counter = 0 counter <- counter + 1 // Mutation allowed // Reference cells are another mutable option let cell = ref 10 cell := 20 // Update content printfn "Cell value: %d" !cell
In the example above, we declare a mutable variable counter
and a reference cell cell. The ref type allows
us to create mutable references to values. The := operator
is used to update the content of the reference cell, while the !
operator is used to dereference it.
Arrays and Reference Types
Arrays and custom classes have reference semantics. Assignments copy references.
let array1 = [| 1; 2; 3 |]
let array2 = array1 // Copies reference
array2.[0] <- 99 // Modifies original
printfn "array1: %A" array1
printfn "array2: %A" array2
type MutablePoint(x: int, y: int) =
member val X = x with get, set
member val Y = y with get, set
let p1 = MutablePoint(1, 2)
let p2 = p1 // Copies reference
p2.X <- 10 // Modifies original
printfn "p1: (%d, %d)" p1.X p1.Y
In the example above, modifying array2 also affects array1,
and modifying p2 affects p1. This is because both
array1 and p1 are references to the same underlying
data.
Parameter Passing
F# follows .NET's pass-by-value approach, but with its immutable focus. Value types are passed by value, while reference types are passed by reference. This means that modifying a value type inside a function does not affect the original, but modifying a reference type does.
let modifyValue x =
let x' = x + 10 // Can't modify original
printfn "Inside function: %d" x'
let modifyArray (arr: int[]) =
arr.[0] <- 100 // Modifies original
printfn "Inside function: %A" arr
let a = 5
modifyValue a
printfn "After modifyValue: %d" a
let nums = [| 1; 2; 3 |]
modifyArray nums
printfn "After modifyArray: %A" nums
In the example above, modifyValue does not change the original
a, while modifyArray modifies the original
nums.
Copying Strategies
Different approaches for copying data structures in F# include shallow copy, deep copy, and copy-and-update. The choice depends on the type of data structure and the desired behavior.
// Records - copy with update
let originalRecord = { Name = "Alice"; Age = 30 }
let copyRecord = { originalRecord with Age = 31 }
// Arrays - clone
let originalArray = [| 1..5 |]
let shallowCopy = Array.copy originalArray
let deepCopy = Array.map id originalArray // Creates new array
// Lists - immutable, so "copy" is just binding
let originalList = [1; 2; 3]
let copyList = originalList // Same list
In the example above, we demonstrate how to create copies of records,
arrays, and lists. Records use the with syntax for copying with
updates, while arrays can be cloned or mapped to create new arrays. Lists
are immutable, so copying is just a reference to the same list.
Summary and Best Practices
- Prefer immutability by default in F#
- Use
mutableonly when necessary - Records and DUs provide safe immutable data structures
- Arrays and classes have reference semantics
- Use copy-and-update syntax for records (
with) - Be explicit about mutation in function signatures
- Consider performance implications of copying large structures
F#'s approach to copying and mutability helps write more predictable and maintainable code by making side effects explicit.
In this article, we explored the concepts of copying values and references in F#. We discussed the differences between value types and reference types, the implications of immutability, and how F# handles parameter passing. We also looked at various copying strategies and best practices for working with data structures in F#. Understanding these concepts is crucial for writing correct and efficient F# code.
Source
Author
List all F# tutorials.