Records: data with names
A record gives every field a name and a type, a copy with one field changed leaves the old value alone, and a list of records is how a table is kept.
Fields with names
A record is a group of named fields: type book = { title: string; pages: int }. Every value of that type has both fields, b.pages is known to be an int, and a misspelled field is a compile error rather than a missing value discovered at run time.
A record is written with the field names, in any order: { title = "Little MLer"; pages = 200 }. Spelling the names at every construction is deliberate — a reader can tell what 200 means without counting positions the way a tuple would force them to. The order is free because the names identify the fields, and the compiler checks that every name belongs to the type.
A change makes a copy
Records are values, so a change produces a new one. { b with pages = 250 } copies every field and replaces the ones named, leaving b itself untouched. The old value is still available, which is what makes a record easy to reason about in a program that passes it around.
A list of records is a table. Every list function applies unchanged: List.map projects a field into a list of titles, List.sort with a comparison on one field orders the rows, and List.iter prints them. Nothing about a record needs a special walk.
How the compiler finds the record type
A record type is nominal: two records with the same fields are still different types, and a field name is resolved using the types the compiler already knows. When the argument type is not obvious, the most recently defined record with that field wins, which is why two record types sharing name in one file can produce a confusing error. Annotating the argument, as in (b : book), removes the guess.
A record value is built all at once. Every field has to be present, and the order does not matter. The with form copies the fields of an existing value and replaces the ones named; it is a new value, so the old one keeps its fields. That is not a performance trick but a consequence of immutability: sharing the unchanged parts is safe because nobody can change them later.
A list of records behaves like any other list. List.map can project one field, List.filter can select rows, List.sort takes a comparison on the fields, and List.fold_left can total a numeric field. The record only changes what the function reads, not how the list is walked.
What a record is at run time
A record is a small block of memory with one slot per field, in the order the type declares them. The field names are not stored — the compiler turns b.pages into the second slot, and the name disappears with the rest of the types. That is why a record costs no more than a tuple with the same fields, and why the names cannot be looked up dynamically at run time.
It also explains the copy. { b with pages = 250 } allocates a new block and copies each slot, replacing the one named. The unchanged values are shared — copying a slot copies a pointer, not a deep clone — but the block itself is new, so a reference to the old block never sees the change.
What goes wrong
Building a record with a missing field is the first error, and the message names the field and the type. There is no default and no empty slot: every field is required, which is the point. If a field really is optional, use an option type and say so.
The second is the field-name guess. With two records sharing name, an unannotated function may resolve b.name against the wrong one, and the error then complains about a type that seems unrelated. Add (b : book) to the parameter; the annotation is not noise, it is the missing fact the compiler could not infer.
Three record traps: building a record with a missing field, which is a compile error rather than a default; trying to assign a field with b.pages <- 250, which does not exist because records are immutable; and letting two record types in one file share a field name, which makes an unannotated function guess the wrong type. Keep field names distinctive or write the type on the argument.
Try these next
- Add a
ratingfield to the book record and print the highest-rated title. - Copy a book with two fields changed at once and check the original with a print.
- Write
titles_of shelfwithList.map, then sort the titles alphabetically.
Copying a record with one change
type point = { x: int; y: int }
let origin = { x = 0; y = 0 }
let right p by = { p with x = p.x + by }
let () = print_endline (string_of_int (right origin 2).x)Two copies, three books
Run it and read the eight lines. Then add a fourth book, sort the shelf by title instead of pages, and print the total page count with a fold.
A playlist with a copy and a total
Run it and read the lines. Then add a track, use a filter and a map to mark only the long tracks as played, and print the total playing time.
When the type cannot be inferred, a field name is resolved against the record type defined most recently in the file, so two records that share a field name make the compiler guess. Keep the names distinctive, or annotate the argument, when a file holds more than one record type.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
How do you read the page count of a book b?
