ZetCode

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:

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:

Program.fs
// 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.

Program.fs
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.

Program.fs
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.

Program.fs
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.

Program.fs
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.

Program.fs
// 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

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

F# Values and Immutability

Immutability in F#

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all F# tutorials.