ZetCode

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.

basic.fsx
[<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.

members.fsx
[<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.

syntax.fsx
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.

tuples.fsx
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.

records.fsx
[<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.

limitations.fsx
[<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.

pattern.fsx
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.

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.