F# Guide — Records: Structured Data with Named Fields
In this tutorial, you will learn about F# Guide. We cover key concepts, practical examples, and best practices to help you master this topic.
F# records are user-defined data types that aggregate values with named fields, providing immutability by default, structural equality, copy-and-update syntax, and seamless integration with pattern matching.
What You'll Learn
- Defining and using record types
- Structural equality and comparison
- Copy-and-update expressions
- Records in pattern matching
- Record vs class differences
Why It Matters
Records are the primary way to model structured data in F#. They combine the simplicity of data containers with functional features like immutability and pattern matching. Durga Antivirus Pro uses records for configuration and rule models.
Real-World Use
Records model domain entities like users, orders, products, and configuration. They are preferred over classes for data-centric types.
flowchart LR
A["Records"] --> B["Definition"]
B --> C["Construction"]
C --> D["Copy & Update"]
D --> E["Pattern Matching"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Defining Records
// Simple record
type Person = {
Name: string
Age: int
Email: string
}
// Record with type parameters
type Result<'T> = {
Value: 'T
Timestamp: System.DateTime
}
// Record with member
type Point = {
X: float
Y: float
} with
member this.DistanceFromOrigin =
sqrt (this.X ** 2 + this.Y ** 2)
Creating Records
// Field order
let alice = { Name = "Alice"; Age = 30; Email = "alice@example.com" }
// Type annotation if ambiguous
let bob: Person = { Name = "Bob"; Age = 25; Email = "bob@example.com" }
// Clone with defaults
type Config = {
Host: string
Port: int
Debug: bool
}
let defaultConfig = { Host = "localhost"; Port = 8080; Debug = false }
Accessing Fields
let person = { Name = "Alice"; Age = 30; Email = "alice@example.com" }
// Dot notation
person.Name // "Alice"
person.Age // 30
// Destructuring in let
let { Name = name; Age = age } = person
// name = "Alice", age = 30
// Destructuring in function parameters
let greet { Name = name } = sprintf "Hello, %s!" name
Structural Equality
// Records have structural equality by default
let a = { Name = "Alice"; Age = 30; Email = "a@example.com" }
let b = { Name = "Alice"; Age = 30; Email = "a@example.com" }
a = b // true (structural equality)
a <> b // false
// This is different from classes (reference equality)
// Useful for comparison and dictionary keys
Copy-and-Update
let alice = { Name = "Alice"; Age = 30; Email = "alice@example.com" }
// Create copy with changed fields
let olderAlice = { alice with Age = 31 }
let nameChange = { alice with Name = "Alice Smith" }
// Multiple changes
let moved = { alice with Email = "alice@newdomain.com"; Age = 31 }
// Original is unchanged
// alice.Age = 30 still
Records in Collections
type Product = { Id: int; Name: string; Price: decimal }
let products = [
{ Id = 1; Name = "Laptop"; Price = 999.99m }
{ Id = 2; Name = "Mouse"; Price = 29.99m }
{ Id = 3; Name = "Keyboard"; Price = 79.99m }
]
let expensiveProducts =
products |> List.filter (fun p -> p.Price > 100m)
let totalValue =
products |> List.sumBy (fun p -> p.Price)
Nested Records
type Address = {
Street: string
City: string
ZipCode: string
}
type Employee = {
Name: string
Address: Address
Department: string
}
let emp = {
Name = "Alice"
Address = { Street = "123 Main"; City = "Portland"; ZipCode = "97201" }
Department = "Engineering"
}
// Nested update
let relocated = { emp with Address = { emp.Address with City = "Seattle" } }
Common Mistakes
1. Forgetting all field values
Records require all fields during construction. No optional fields exist (use option types).
2. Ambiguous record types
Two record types with same field names cause ambiguity. Use type annotations or different field names.
3. Expecting mutation
Records are immutable. Use copy-and-update for modifications.
4. Overusing record members
Keep records as simple data containers. Add methods sparingly.
5. Confusing records with anonymous records
F# also has anonymous records {| Name: string |} with different semantics (no type definition needed).
Practice Questions
1. What is structural equality? Two records with the same field values are equal. Unlike classes, which use reference equality by default.
2. How do you create a copy with modified fields?
Use copy-and-update: { originalRecord with Field = newValue }.
3. Can records have methods?
Yes, use with member this.MethodName = ... in the type definition.
Challenge: Design a record hierarchy for an e-commerce system with Customer, Order, and Product records.
FAQ
{{< faq question="Are records reference types?" >}}
Yes, F# records are reference types stored on the heap. For value-type records, add [<Struct>] attribute.
{{< /faq >}}
{{< faq question="Can records implement interfaces?" >}}
Yes, records can implement interfaces using the interface ... with syntax.
{{< /faq >}}
{{< faq question="Do records support inheritance?" >}} No. Records cannot inherit from other records. Use discriminated unions or classes for inheritance hierarchies. {{< /faq >}}
{{< faq question="How are records different from tuples?" >}} Records have named fields (self-documenting) and can have many fields. Tuples are positional and best for 2-3 values. {{< /faq >}}
{{< faq question="Can records be mutable?" >}}
Yes, add mutable keyword to fields: { mutable Name: string }. But this goes against F# idioms.
{{< /faq >}}
Mini Project
Build a contact management system with records:
type Phone = { Type: string; Number: string }
type Contact = {
Name: string
Email: string option
Phones: Phone list
}
let contacts = [
{ Name = "Alice"; Email = Some "alice@example.com"; Phones = [{ Type = "Mobile"; Number = "555-0100" }] }
{ Name = "Bob"; Email = None; Phones = [{ Type = "Home"; Number = "555-0200" }; { Type = "Work"; Number = "555-0201" }] }
]
let getEmailOrPhone contact =
match contact.Email with
| Some email -> email
| None -> (List.head contact.Phones).Number
What's Next
Now that you understand records, explore discriminated unions for modeling choices and variants.
| Topic | Description | Link |
|---|---|---|
| F# Discriminated Unions | Union types | {{< ref "11-discriminated-unions" >}} |
| F# Option Types | Option type deep dive | {{< ref "12-option-types" >}} |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro