100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Tuples and Records

Learn F#'s two core structural product types -- quick, unnamed tuples and named, documented records -- and when to reach for each.

Core ConceptsBeginner8 min readJul 10, 2026
Analogies

Tuples: Quick, Unnamed Groupings

A tuple groups a fixed number of values of possibly different types into a single value without naming any of them, written as a comma-separated list in parentheses, such as (1, "hello", true), whose inferred type is int * string * bool. Tuples are structurally equal -- two tuples are equal if they have the same length and every corresponding element is equal -- and they're immutable, so there's no way to mutate one element of a tuple in place.

🏏

Cricket analogy: A quick scoring note like (4, "boundary", false) bundling runs, shot type, and whether it was a six is grouped instantly without a formal template, mirroring how an F# tuple like (1, "hello", true) groups values positionally without naming any of them.

Tuples are the idiomatic way to return multiple values from a function without declaring a dedicated type: "let divmod a b = (a / b, a % b)" returns both the quotient and remainder as a single 2-tuple, and callers destructure the result directly with "let (q, r) = divmod 17 5". This same destructuring pattern works directly in function parameters and for loops, letting you unpack a tuple's elements into named bindings wherever it appears.

🏏

Cricket analogy: A scoreboard operator reads off both the total and the wicket count in one glance and immediately assigns each to its own display panel, mirroring how let (q, r) = divmod 17 5 destructures a tuple's two return values into separate named bindings.

fsharp
// Tuple: quick, unnamed grouping
let point = (3, 4)
let divmod a b = (a / b, a % b)
let (quotient, remainder) = divmod 17 5
printfn "%d r%d" quotient remainder     // 3 r2

// Record: named fields, structural equality
type Person = { Name: string; Age: int }

let ada = { Name = "Ada"; Age = 30 }
let grace = { Name = "Grace"; Age = 30 }
printfn "%b" (ada = grace)              // false, Name differs

// copy-and-update expression
let adaNextYear = { ada with Age = ada.Age + 1 }
printfn "%A" adaNextYear                // { Name = "Ada"; Age = 31 }

Records: Named Fields with Structural Equality

A record type names its fields explicitly, such as "type Person = { Name: string; Age: int }", giving both compile-time documentation of the shape and, unlike tuples, protection against accidentally swapping two same-typed fields since each field is accessed by name (person.Name) rather than by position. Records also get structural equality and a readable default ToString for free -- { Name = "Ada"; Age = 30 } = { Name = "Ada"; Age = 30 } evaluates to true because F# compares every field's value rather than reference identity.

🏏

Cricket analogy: A named player profile card listing Name, Team, and BattingAverage explicitly, rather than three unlabeled numbers, mirrors an F# record like type Person = { Name: string; Age: int }, and two identically filled-out profile cards are considered the same player mirrors structural equality.

Structural equality means = compares records and tuples field-by-field/element-by-element rather than by reference, so two separately constructed records with identical field values are equal -- a sharp contrast to reference types in C#, where == compares identity unless equality is explicitly overridden.

The copy-and-update Expression

Because records are immutable by default, changing a single field requires the copy-and-update expression, { existingRecord with FieldName = newValue }, which creates an entirely new record sharing every unchanged field's value with the original while replacing only the specified fields -- for example { person with Age = person.Age + 1 } produces a new Person one year older without mutating the original binding at all. Multiple fields can be updated in the same expression by separating them with semicolons inside the with clause.

🏏

Cricket analogy: Updating a player's profile card to bump only the BattingAverage field after a new innings, while every other field like Name and Team stays exactly as printed, mirrors { person with Age = person.Age + 1 }, which produces a new record sharing all unchanged fields.

Because F# records rely on field names for construction ({ Name = "Ada"; Age = 30 }), having two record types in scope that share a field name can cause the compiler to infer the wrong record type for a literal expression, especially with open-imported modules; disambiguate with an explicit type annotation or the fully qualified type name when this happens.

Tuples vs Records: When to Use Each

Reach for a tuple when the grouping is local, short-lived, and self-explanatory from context -- like a function's return value used immediately by its caller -- and reach for a record when the data crosses a function boundary, gets stored, gets passed around broadly, or would be confusing without field names, since a record's named fields serve as inline documentation that a string * int * bool tuple simply cannot provide to a future reader.

🏏

Cricket analogy: A quick scribbled note of (4, 6) for two consecutive balls' runs is fine in the moment, but the official scorebook uses named columns like Over, Ball, and Runs precisely because it's read by others later, mirroring choosing a tuple for local use versus a record for anything that gets stored or shared.

  • Tuples group a fixed number of values positionally, without field names, e.g. int * string.
  • Tuples are the idiomatic way to return multiple values from a function.
  • Records name every field explicitly, giving documentation and preventing positional mix-ups.
  • Both tuples and records have structural equality by default, comparing values not references.
  • Updating a record uses the copy-and-update { x with Field = v } expression, producing a new record.
  • Ambiguous field names across multiple record types in scope can confuse type inference.
  • Use tuples for short-lived, local groupings; use records for data that crosses boundaries or is stored.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#FStudyNotes#TuplesAndRecords#Tuples#Records#Quick#Unnamed#DataStructures#StudyNotes#SkillVeris