Dive C1.06.1
Records
A value that has field A and field B at once — named, immutable, compared by its contents.
A record is F#'s product type: it groups a fixed set of named fields into one
value. The declaration type Point = { X: float; Y: float } names the type and its two
fields; a value is built by naming every field, and F# reads the record type back off those names — no annotation
needed. Records are immutable: there is no statement that changes a field in place. To get a
changed value you build a new one with the copy-and-update expression
{ p with Y = 3.0 }, which copies p and replaces the named
field, leaving p exactly as it was. And two records are equal when their fields are
equal — structural equality comes for free.
Declare it, build it
The declaration lists the fields and their types between braces. To build a value, name every field and give
it a value; the order of the fields in the expression does not matter. Because the field names
X and Y belong to exactly one declared record, F# infers
the type Point without a hint.
type Point = { X: float; Y: float } let p = { X = 1.0; Y = 2.0 } // val p : Point = { X = 1.0; Y = 2.0 }
A field is read with dot notation, the same as any .NET member: p.X is
1.0. Nothing about the value can change after this line; the next part shows how to
get a different value without mutating this one.
- record
- a product type: one value holds a fixed set of named fields, all present at once.
- { X = …; Y = … }
- a record expression — name every field; F# infers the type from the names.
- p.X
- field access by name, with dot notation.
- immutable
- no field can be reassigned in place; a different value is a different record.
Copy-and-update returns a new record
The expression { p with Y = 3.0 } reads "a copy of p, with
Y set to 3.0". It produces a new
Point; p is untouched. Below, pick a field to change and a
new value for it. The original record stays fixed on the left; the copy on the right shows the result, with the
changed field highlighted. The equality readout confirms the two are now different values.
{ p with <field> = <value> } · p stays · a new record comes back
Copy-and-update never edits the old record; it returns a new one. The original keeps its value, so a reference held elsewhere never changes underfoot.
The same in FSI: bind the result of { p with Y = 3.0 } to a name, then read
p back to confirm it did not move.
let p2 = { p with Y = 3.0 } // val p2 : Point = { X = 1.0; Y = 3.0 } p // val it : Point = { X = 1.0; Y = 2.0 } (p never changed)
Equality is structural
Two records are equal when their fields are equal — F# generates the comparison from the type, so
= works on records out of the box. This is why the copy above is "different from"
p: one field differs, so the whole values differ. Two separately built records with
the same contents are equal.
let a = { X = 1.0; Y = 2.0 } let b = { X = 1.0; Y = 2.0 } a = b // val it : bool = true (same contents, equal) p = p2 // val it : bool = false (Y differs)
{| X = 1; Y = 2 |}. They need no type declaration — the type
is the shape. FSI gives the value above the structural type {| X: int; Y: int |}.
They suit a one-off shape, such as the result of a query, where a named record would be overkill.
Product type
A value carries field A and field B and field C — every field present at once.
F# record
{ X: float; Y: float } — named, immutable, copy-and-updated, compared by contents.
Where this leads
A record is the and. The next dive builds the or: a discriminated union, where a value is one of a closed set of cases rather than all the fields at once. Together the two cover every shape a domain needs, and the third dive uses both to make a wrong value impossible to write.
References
Sources
- Microsoft — F# documentation — records, the copy-and-update expression, and anonymous records.
- The F# Language Specification — record type definitions, record expressions, and structural equality.
- FSharp.Core API reference — the generated equality and comparison the compiler attaches to records.
Related in this course
- C1.06.2 — Discriminated unions — the sum type that complements the record.
- C1.01 — Values, types & FSI — the inference and the FSI
val itecho this dive leans on. - Elixir — Algebraic data types — products built from maps on the BEAM.