Version: 0.23.0

Records and Maps

So far, we have seen pretty basic data types. LIGO also offers more complex built-in constructs, such as records and maps.

Records#

Records are one-way data of different types can be packed into a single type. A record is made of a set of fields, which are made of a field name and a field type. Given a value of a record type, the value bound to a field can be accessed by giving its field name to a special operator (.).

Let us first consider an example of record type declaration.

type user = {
id : nat,
is_admin : bool,
name : string
};

And here is how a record value is defined:

let alice : user = {
id : 1 as nat,
is_admin : true,
name : "Alice"
};

Accessing Record Fields#

If we want the contents of a given field, we use the (.) infix operator, like so:

let alice_admin: bool = alice.is_admin;

Functional Updates#

Given a record value, it is a common design pattern to update only a small number of its fields. Instead of copying the fields that are unchanged, LIGO offers a way to only update the fields that are modified.

One way to understand the update of record values is the functional update. The idea is to have an expression whose value is the updated record.

Let us consider defining a function that translates three-dimensional points on a plane.

The syntax for the functional updates of record in JsLIGO:

type point = {x: int, y: int, z: int};
type vector = {dx: int, dy: int};
let origin: point = {x: 0, y: 0, z: 0};
let xy_translate = ([p, vec]: [point, vector]): point =>
({...p, x: p.x + vec.dx, y: p.y + vec.dy});

You can call the function xy_translate defined above by running the following command of the shell:

ligo evaluate-call
gitlab-pages/docs/language-basics/src/maps-records/record_update.jsligo
xy_translate "({x:2,y:3,z:1}, {dx:3,dy:4})"
# Outputs: {z = 1 , y = 7 , x = 5}

You have to understand that p has not been changed by the functional update: a nameless new version of it has been created and returned.

Nested updates#

A unique feature of LIGO is the ability to perform nested updates on records. JsLIGO however does not support the specialised syntax as the other syntaxes. The following however also does the trick.

For example if you have the following record structure:

type color =
["Blue"]
| ["Green"];
type preferences = {
color: color,
other: int
};
type account = {
id: int,
preferences: preferences
};

You can update the nested record with the following code:

let change_color_preference = (account : account, color : color): account =>
({ ...account, preferences: {...account.preferences, color: color }});

Note that all the records in the path will get updated. In this example that's account and preferences.

You can call the function change_color_preference defined above by running the following command:

ligo evaluate-call gitlab-pages/docs/language-basics/src/maps-records/record_nested_update.ligo
change_color_preference "(record [id=1001; preferences=record [color=Blue; other=1]], Green)"
# Outputs: record[id -> 1001 , preferences -> record[color -> Green(unit) , other -> 1]]

Comparison#

Record types are comparable, which allows to check for equality and use records as key in sets or maps. By default, the ordering of records is undefined and implementation dependent. Ultimately, the order is determined by the translated Michelson type. When using the [@layout:comb] attribute, fields are translated in their order in the record, and records are then ordered with lexicographic ordering.

Maps#

Maps are a data structure which associate values of the same type to values of the same type. The former are called key and the latter values. Together they make up a binding. An additional requirement is that the type of the keys must be comparable, in the Michelson sense.

Declaring a Map#

Here is how a custom map from addresses to a pair of integers is defined.

type move = [int, int];
type register = map<address, move>;

Creating an Empty Map#

Here is how to create an empty map.

let empty: register = Map.empty;

Creating a Non-empty Map#

And here is how to create a non-empty map value:

let moves : register =
Map.literal (list([
["tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" as address, [1,2]],
["tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" as address, [0,3]]]));

The Map.literal predefined function builds a map from a list of key-value pair tuples, [<key>, <value>]. Note also the , to separate individual map entries. "<string value>" as address means that we type-cast a string into an address.

Accessing Map Bindings#

let my_balance: option<move> =
Map.find_opt("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" as address, moves);

Notice how the value we read is an optional value: this is to force the reader to account for a missing key in the map. This requires pattern matching.

let force_access = ([key, moves]: [address, register]): move => {
return match(Map.find_opt (key, moves), {
Some: (move: register) => move,
None: () => (failwith("No move.") as move)
});
};

Updating a Map#

Given a map, we may want to add a new binding, remove one, or modify one by changing the value associated to an already existing key. All those operations are called updates.

We can update a binding in a map in JsLIGO by means of the Map.update built-in function:

let assign = (m: register): register =>
Map.update
("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" as address, Some ([4, 9]), m);

Notice the optional value Some ([4,9]) instead of [4, 9]. If we used None instead that would have meant that the binding is removed.

As a particular case, we can only add a key and its associated value.

let add = (m: register): register =>
Map.add
("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" as address, [4, 9], m);

To remove a binding from a map, we need its key.

In JsLIGO, we use the predefined function Map.remove as follows:

let delete = ([key, moves]: [address, register]): register =>
Map.remove(key, moves);

Functional Iteration over Maps#

A functional iterator is a function that traverses a data structure and calls in turn a given function over the elements of that structure to compute some value. Another approach is possible in PascaLIGO: loops (see the relevant section).

There are three kinds of functional iterations over LIGO maps: the iterated operation, the map operation (not to be confused with the map data structure) and the fold operation.

Iterated Operation over Maps#

The first, the iterated operation, is an iteration over the map with no return value: its only use is to produce side-effects. This can be useful if, for example you would like to check that each value inside of a map is within a certain range and fail with an error otherwise.

The predefined functional iterator implementing the iterated operation over maps is called Map.iter. In the following example, the register of moves is iterated to check that the start of each move is above 3.

let iter_op = (m: register): unit => {
let predicate = ([i, j]: [address, move]): unit => assert(j[0] > 3);
Map.iter(predicate, m);
};

Map Operations over Maps#

We may want to change all the bindings of a map by applying to them a function. This is called a map operation, not to be confused with the map data structure. The predefined functional iterator implementing the map operation over maps is called Map.map. In the following example, we add 1 to the ordinate of the moves in the register.

let map_op = (m: register): register => {
let increment = ([i, j]: [address, move]): [int, int] => [j[0], j[1] + 1];
return Map.map(increment, m);
};

Folded Operations over Maps#

A folded operation is the most general of iterations. The folded function takes two arguments: an accumulator and the structure element at hand, with which it then produces a new accumulator. This enables having a partial result that becomes complete when the traversal of the data structure is over.

The predefined functional iterator implementing the folded operation over maps is called Map.fold and is used as follows.

let fold_op = (m: register): int => {
let folded = ([i, j]: [int, [address, move]]): int => i + j[1][1];
return Map.fold(folded, m, 5);
};

Big Maps#

Ordinary maps are fine for contracts with a finite lifespan or a bounded number of users. For many contracts however, the intention is to have a map holding many entries, potentially millions of them. The cost of loading those entries into the environment each time a user executes the contract would eventually become too expensive were it not for big maps. Big maps are a data structure offered by Michelson which handles the scaling concerns for us. In LIGO, the interface for big maps is analogous to the one used for ordinary maps.

Declaring a Map#

Here is how we define a big map:

type move = [int, int];
type register = big_map<address, move>;

Creating an Empty Big Map#

Here is how to create an empty big map.

let empty: register = Big_map.empty;

Creating a Non-empty Map#

And here is how to create a non-empty map value:

let moves : register =
Big_map.literal (list([
["tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" as address, [1, 2]],
["tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" as address, [0, 3]]]));

The predefined function Big_map.literal constructs a big map from a list of key-value pairs [<key>, <value>]. Note also the semicolon separating individual map entries. The annotated value ("<string> value>" as address) means that we cast a string into an address.

Accessing Values#

If we want to access a move from our register above, we can use the postfix [] operator to read the associated move value. However, the value we read is an optional value (in our case, of type option (move)), to account for a missing key. Here is an example:

let my_balance: option<move> =
Big_map.find_opt("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" as address, moves);

Updating Big Maps#

We can update a big map in JsLIGO using the Big_map.update built-in:

let updated_map: register =
Big_map.update
("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" as address, Some([4, 9]), moves);

Removing Bindings#

Removing a binding in a map is done differently according to the LIGO syntax.

In JsLIGO, the predefined function which removes a binding in a map is called Map.remove and is used as follows:

let updated_map: register =
Map.remove("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" as address, moves);