For AI agents: Documentation index at /llms.txt

Skip to content

Records

Records allow you to group related values using named fields, with each field potentially having a different type. Unlike tuples, which use positional access, records provide field-based access, improving readability and maintainability.

Records also support mutable fields, declared using the var keyword. In contrast, all fields in a tuple are always immutable.

let person = {
name : Text = "Motoko";
age : Nat = 25;
};

person is a record with two labeled fields, name of type Text and age of type Nat.

The values of the fields are "Motoko" and 25 respectively.

The type of the person value is {age : Nat; name : Text}, or, equivalently, {name: Text; age : Nat}. Unlike tuples, the order of record fields is immaterial and all record types with the same field names and types are considered equivalent, regardless of field ordering.

Fields in a record can be accessed using the dot (.) notation.

let person = {
name : Text = "Motoko";
age : Nat = 25;
};
let personName = person.name; // "Motoko"
let personAge = person.age; // 25
person;

Attempting to access a field that isn’t available in the type of the record is a compile-time type error.

By default, record fields are immutable. To create a mutable field, use var.

let person = {
var name : Text = "Motoko";
var age : Nat = 25;
};

This person has type {var age : Nat; var name : Text}.

var name and var age allow the values to be updated later.

let person = {
var name : Text = "Motoko";
var age : Nat = 25;
};
person.name := "Ghost"; // Now person.name is "Ghost"
person.age := 30; // Now person.age is 30
person

Attempting to update an immutable field is a compile-time type error.

Records can contain other records, allowing for hierarchical data structures that maintain organization while ensuring type safety and clarity.

type Address = {
city : Text;
street : Text;
zip : Nat;
};
type Individual = {
name : Text;
address : Address;
};
let individual : Individual = {
name = "Motoko";
address = { street = "101 Broadway"; city = "New York"; zip = 10001 };
};

Records can be destructured using switch, allowing selective extraction of fields. This approach makes accessing deeply nested fields more explicit and readable.

type Address = {
city : Text;
street : Text;
zip : Nat;
};
type Individual = {
name : Text;
address : Address;
};
let individual : Individual = {
name = "Motoko";
address = { street = "101 Broadway"; city = "New York"; zip = 10001 };
};
let { name; address = { city = cityName }} = individual;

The pattern both defines name as the contents of eponymous field individual.name and cityName as the contents of field individual.address.city. Irrelevant fields need not be listed in the pattern. The field pattern name is just shorthand for name = name: it binds the contents of the field called name, to the variable called name.

Records are commonly used in arrays and other collections for structured data storage, allowing efficient data organization and retrieval.

type Product = {
name : Text;
price : Float;
};
let inventory : [Product] = [
{ name = "Laptop"; price = 999.99 },
{ name = "Smartphone"; price = 599.99 }
];
let firstProduct : Product = inventory[0];
let productName : Text = firstProduct.name;

Since records are immutable by default, updating a record requires creating a modified copy. Motoko allows combining and extending records using the and and with keywords.

The and keyword merges multiple records when they have no conflicting fields.

let contact = { email : Text = "motoko@example.com"; };
let person = { name : Text = "Motoko"; age : Nat = 25; };
let profile = { person and contact };
debug_show(profile);

profile combines person and contact because they have unique fields. If any field name overlaps, and alone is not allowed. Use the with keyword to resolve conflicts.

Overriding and extending records using with

Section titled “Overriding and extending records using with”

The with keyword modifies, overrides, or adds fields when combining records.

let person = { name : Text = "Motoko"; age : Nat = 25; };
// age = 26; updates the existing age field.
// city = "New York" adds a new field to the record.
// city = "New York" adds a new field to the record.
let updatedPerson = { person with age : Nat = 26; city : Text = "New York"; };
debug_show(updatedPerson);
let person = {name : Text = "Motoko"; age : Nat = 25};
let contact = {email : Text = "motoko@example.com"};
// profile and contact merge with and since they have unique fields.
// age = 26; updates the age field from profile.
// location = "New York"; adds a new field.
let fullProfile = {
person and contact with
age = 26;
location : Text = "New York";
};
debug_show(fullProfile);

Tuples and records both allow grouping values, but they have key differences in structure, mutability, and field access. While tuples provide a compact way to group values, records offer more flexibility for structured data modeling, especially when dealing with complex relationships or named fields.

FeatureTupleRecord
StructureOrdered collection of valuesUnordered collection of named fields
ProjectionBy position (.n)By field name (.fieldName)
Pattern matchingComplete, using ordered tuple patternsSelective, using unordered record patterns
MutabilityImmutable after creationCan have mutable fields
NamingFields are anonymousFields are named
SubtypingFields cannot be removedFields can be removed
Use casePositional grouping of related values, e.g. vectorsStructured data types

Motoko’s records support more flexible subtyping than tuples. With records, subtyping allows fields to be omitted in the subtype (a concept known as width subtyping). In contrast, tuple subtyping requires tuples to have the same length, making them less flexible in this regard.

For example, {x : Int, y : Int, z : Int} is a subtype of {x : Int, y : Int}, but (Int, Int, Int) is not a subtype of (Int, Int).