F# struct types
last modified May 3, 2025
In this article, we explore struct types in F#. Structs are value types that provide efficient memory usage and performance benefits in specific scenarios.
A struct in F# is a value type that inherits from
System.ValueType
, meaning it is handled differently from reference
types such as classes. Unlike objects, which are allocated on the heap and
managed by the garbage collector, structs are typically stored on the stack or
embedded within other structures. This distinction makes structs particularly
useful for scenarios where minimizing allocation overhead and improving
performance are priorities.
Structs are most effective when used for small, frequently used data structures, such as coordinates, color representations, or simple mathematical constructs. Since structs avoid heap allocation, they help reduce memory pressure and improve efficiency—especially in performance-critical applications.
In F#, structs can be defined using the [<Struct>]
attribute
or the struct ... end
syntax. The [<Struct>]
attribute is applied to standard type definitions, signaling that the type
should be treated as a value type. The struct ... end
syntax
provides a more explicit way to declare structs, ensuring they function as
intended within F#'s type system.
However, structs come with certain limitations compared to reference types. They
do not support inheritance (other than from System.ValueType
),
meaning they cannot be used in class hierarchies. Additionally, modifying a
struct after it has been passed to a function or assigned to a variable requires
special attention because structs are copied by value rather than by reference.
F# basic struct type
The simplest form of struct type definition and usage.
[<Struct>] type Point2D = val X: float val Y: float new(x, y) = { X = x; Y = y } let p = Point2D(1.0, 2.0) printfn "Point: (%.1f, %.1f)" p.X p.Y
We define a simple 2D point struct and create an instance of it.
[<Struct>] type Point2D =
The [<Struct>] attribute marks this as a value type.
val X: float val Y: float
Struct fields are explicitly declared with val.
λ dotnet fsi basic.fsx Point: (1.0, 2.0)
F# struct with members
Structs can have methods and properties like classes.
[<Struct>] type Vector2D = val X: float val Y: float new(x, y) = { X = x; Y = y } member this.Length = sqrt(this.X * this.X + this.Y * this.Y) member this.Add(v: Vector2D) = Vector2D(this.X + v.X, this.Y + v.Y) let v1 = Vector2D(3.0, 4.0) let v2 = Vector2D(1.0, 2.0) let v3 = v1.Add(v2) printfn "v1 length: %.2f" v1.Length printfn "v3: (%.1f, %.1f)" v3.X v3.Y
We define a vector struct with methods and properties.
member this.Length = sqrt(this.X * this.X + this.Y * this.Y)
Structs can have computed properties like classes.
λ dotnet fsi members.fsx v1 length: 5.00 v3: (4.0, 6.0)
F# struct alternative syntax
F# provides an alternative syntax for defining structs.
type Point3D = struct val X: float val Y: float val Z: float new(x, y, z) = { X = x; Y = y; Z = z } end let p = Point3D(1.0, 2.0, 3.0) printfn "Point: (%.1f, %.1f, %.1f)" p.X p.Y p.Z
Shows the alternative struct...end syntax for defining structs.
type Point3D = struct ... end
This is equivalent to using the [<Struct>] attribute.
λ dotnet fsi syntax.fsx Point: (1.0, 2.0, 3.0)
F# Struct Tuples
Struct tuples provide a lightweight alternative to regular tuples by ensuring value-type semantics. Unlike standard tuples, which are reference types and allocated on the heap, struct tuples are stored directly in memory, making them more efficient for performance-sensitive operations.
let regularTuple = (1, "hello", 3.14) let structTuple = struct (1, "hello", 3.14) let processStructTuple (t: struct (int, string, float)) = match t with | struct (a, b, c) -> printfn "Struct tuple: %d, %s, %f" a b c let processRegularTuple (t: int * string * float) = match t with | (a, b, c) -> printfn "Regular tuple: %d, %s, %f" a b c processStructTuple structTuple processRegularTuple regularTuple
In this example, a regular tuple and a struct tuple are
created with identical values. The struct tuple is explicitly defined using the
struct
keyword, ensuring it behaves as a value type. Because struct
tuples and regular tuples have different type representations, we use separate
functions to process each. The explicit type annotations in function parameters
prevent pattern-matching issues that occur when attempting to handle both in the
same match expression.
λ dotnet fsi tuples.fsx Struct tuple: 1, hello, 3.140000 Regular tuple: 1, hello, 3.140000
F# struct records
Records can be defined as structs for better performance.
[<Struct>] type Person = { Name: string Age: int } let p1 = { Name = "John"; Age = 30 } let p2 = { p1 with Name = "Jane" } printfn "Person: %s, %d" p1.Name p1.Age printfn "Person: %s, %d" p2.Name p2.Age
Shows how to define and use struct records.
[<Struct>] type Person =
The [<Struct>] attribute makes this a value type record.
λ dotnet fsi records.fsx Person: John, 30 Person: Jane, 30
F# struct limitations
Structs have several important limitations to consider.
[<Struct>] type LimitedStruct = val X: int val Y: string // Structs can have reference type fields new(x, y) = { X = x; Y = y } // Structs cannot use let bindings for fields // [<Struct>] // type InvalidStruct = // let x = 5 // Compilation error let s = LimitedStruct(1, "test") printfn "Struct: %d, %s" s.X s.Y
Demonstrates some limitations of struct types in F#.
val Y: string // Structs can have reference type fields
Structs can contain reference types but lose some benefits.
λ dotnet fsi limitations.fsx Struct: 1, test
F# struct pattern matching
Structs can be used with pattern matching like other types.
open System [<Struct>] type Shape = | Circle of radius: float | Rectangle of width: float * height: float let area shape = match shape with | Circle r -> Math.PI * r * r | Rectangle (w, h) -> w * h let circle = Circle(5.0) let rect = Rectangle(4.0, 6.0) printfn "Circle area: %.2f" (area circle) printfn "Rectangle area: %.2f" (area rect)
Shows how to define and pattern match against struct discriminated unions.
| Circle of radius: float
Struct discriminated unions are defined the same way as regular ones.
λ dotnet fsi pattern.fsx Circle area: 78.54 Rectangle area: 24.00
In this article we've explored struct types in F#, their benefits, limitations, and appropriate use cases. Structs are a valuable tool for performance-sensitive code where value semantics are desired.