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.
// 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
1. What is the inferred type of the tuple (1, "hello", true)?
2. What does the copy-and-update expression { person with Age = 31 } do?
3. How are two records compared with the = operator by default?
4. Why might you prefer a record over a tuple for a function's parameter?
5. What is the idiomatic way to return two values from an F# function?
Was this page helpful?
You May Also Like
Functions in F#
Learn how F# treats functions as first-class values, covering let bindings, currying, partial application, and higher-order functions.
Type Inference in F#
See how F#'s Hindley-Milner-based type inference deduces precise types without annotations, and when explicit annotations are still needed.
Lists and Sequences
Compare F#'s immutable linked-list list type with the lazy, deferred seq type, and learn the core operations for building and transforming both.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics