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
let
creates immutable values - Mutable variables: Must be explicitly declared with
mutable
keyword - 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
mutable
only 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.