Skip to main 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.

Defining a record

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

Type annotations on immutable record fields are optional, as the compiler can infer their types automatically. However, for mutable fields (var), it's good practice to explicitly declare the type. Without an explicit annotation, the compiler might infer a more specific type than intended, which can restrict future assignments to broader types.

Example:

let account = { name = "Motoko"; var balance = 0 };
// Inferred as { name : Text; var balance : Nat }
account.balance := -100; // Rejected, -100 is an Int not a Nat

To avoid this issue, annotate the field explicitly:

let account = { name = "Motoko"; var balance : Int = 0 };
account.balance := -100; // Allowed

This recommendation also applies to all var declarations, not just record fields.

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.

Accessing fields

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.

Record mutability

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.

Nested records

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 };
};

Pattern matching on records

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.

Using records in collections

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;

Updating records programmatically

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.

Merging records with and

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

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);

If person contained a mutable (var) field, with must redefine it, preventing aliasing.

Combining and and with

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 vs records

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).