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 is
record [
id : nat;
is_admin : bool;
name : string
]

And here is how a record value is defined:

const alice : user =
record [
id = 1n;
is_admin = True;
name = "Alice"
]

Accessing Record Fields

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

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

In PascaLIGO, the shape of that expression is <record variable> with <record value>. The record variable is the record to update, and the record value is the update itself.

type point is record [x : int; y : int; z : int]
type vector is record [dx : int; dy : int]
const origin : point = record [x = 0; y = 0; z = 0]
function xy_translate (var p : point; const vec : vector) : point is
p with record [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 run-function
gitlab-pages/docs/language-basics/src/maps-records/record_update.ligo
xy_translate "(record [x=2;y=3;z=1], record [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 by the block-less function.

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

ligo run-function
gitlab-pages/docs/language-basics/src/maps-records/record_update.religo
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.

For example if you have the following record structure:

type color is
| Blue
| Green
type preferences is record [
color : color;
other : int;
]
type account is record [
id : int;
preferences : preferences;
]

You can update the nested record with the following code:

function change_color_preference (const account : account; const color : color ) : account is
block {
account := account with record [preferences.color = color]
} with account

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 run-function 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]]

Record Patches

Another way to understand what it means to update a record value is to make sure that any further reference to the value afterward will exhibit the modification. This is called a patch and this is only possible in PascaLIGO, because a patch is an instruction, therefore we can only use it in a block. Similarly to a functional update, a patch takes a record to be updated and a record with a subset of the fields to update, then applies the latter to the former (hence the name "patch").

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

type point is record [x : int; y : int; z : int]
type vector is record [dx : int; dy : int]
function xy_translate (var p : point; const vec : vector) : point is
block {
patch p with record [x = p.x + vec.dx];
patch p with record [y = p.y + vec.dy]
} with p

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

ligo run-function
gitlab-pages/docs/language-basics/src/maps-records/record_patch.ligo
xy_translate "(record [x=2;y=3;z=1], record [dx=3;dy=4])"
# Outputs: {z = 1 , y = 7 , x = 5}

Of course, we can actually translate the point with only one patch, as the previous example was meant to show that, after the first patch, the value of p indeed changed. So, a shorter version would be

type point is record [x : int; y : int; z : int]
type vector is record [dx : int; dy : int]
function xy_translate (var p : point; const vec : vector) : point is
block {
patch p with record [x = p.x + vec.dx; y = p.y + vec.dy]
} with p

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

ligo run-function
gitlab-pages/docs/language-basics/src/maps-records/record_patch2.ligo
xy_translate "(record [x=2;y=3;z=1], record [dx=3;dy=4])"
# Outputs: {z = 1 , y = 7 , x = 5}

Record patches can actually be simulated with functional updates. All we have to do is declare a new record value with the same name as the one we want to update and use a functional update, like so:

type point is record [x : int; y : int; z : int]
type vector is record [dx : int; dy : int]
function xy_translate (var p : point; const vec : vector) : point is
block {
const p : point = p with record [x = p.x + vec.dx; y = p.y + vec.dy]
} with p

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

ligo run-function
gitlab-pages/docs/language-basics/src/maps-records/record_simu.ligo
xy_translate "(record [x=2;y=3;z=1], record [dx=3;dy=4])"
# Outputs: {z = 1 , y = 7 , x = 5}

The hiding of a variable by another (here p) is called shadowing.

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 is int * int
type register is map (address, move)

Creating an Empty Map

Here is how to create an empty map.

const empty : register = map []

Creating a Non-empty Map

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

const moves : register =
map [
("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" : address) -> (1,2);
("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address) -> (0,3)]

Notice the -> between the key and its value and ; to separate individual map entries. The annotated value ("<string value>" : address) means that we cast a string into an address. Also, map is a keyword.

Accessing Map Bindings

In PascaLIGO, we can use the postfix [] operator to read the move value associated to a given key (address here) in the register. Here is an example:

const my_balance : option (move) =
moves [("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address)]

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.

function force_access (const key : address; const moves : register) : move is
case moves[key] of
Some (move) -> move
| None -> (failwith ("No move.") : move)
end

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.

The values of a PascaLIGO map can be updated using the usual assignment syntax <map variable>[<key>] := <new value>. Let us consider an example.

function assign (var m : register) : register is
block {
m [("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address)] := (4,9)
} with m

If multiple bindings need to be updated, PascaLIGO offers a patch instruction for maps, similar to that for records.

function assignments (var m : register) : register is
block {
patch m with map [
("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address) -> (4,9);
("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" : address) -> (1,2)
]
} with m

See further for the removal of bindings.

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

In PascaLIGO, there is a special instruction to remove a binding from a map.

function delete (const key : address; var moves : register) : register is
block {
remove key from map moves
} with 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.

function iter_op (const m : register) : unit is
block {
function iterated (const i : address; const j : move) : unit is
if j.1 > 3 then Unit else (failwith ("Below range.") : unit)
} with Map.iter (iterated, m)

Note that map_iter is deprecated.

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.

function map_op (const m : register) : register is
block {
function increment (const i : address; const j : move) : move is
(j.0, j.1 + 1)
} with Map.map (increment, m)

Note that map_map is deprecated.

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.

function fold_op (const m : register) : int is
block {
function folded (const i : int; const j : address * move) : int is
i + j.1.1
} with Map.fold (folded, m, 5)

Note that map_fold is deprecated.

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 is int * int
type register is big_map (address, move)

Creating an Empty Big Map

Here is how to create an empty big map.

const empty : register = big_map []

Creating a Non-empty Map

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

const moves : register =
big_map [
("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" : address) -> (1,2);
("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address) -> (0,3)]

Notice the right arrow -> between the key and its value and the semicolon separating individual map entries. The value annotation ("<string value>" : 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:

const my_balance : option (move) =
moves [("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address)]

Updating Big Maps

The values of a PascaLIGO big map can be updated using the assignment syntax for ordinary maps

function assign (var m : register) : register is
block {
m [("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address)] := (4,9)
} with m

If multiple bindings need to be updated, PascaLIGO offers a patch instruction for maps, similar to that for records.

function assignments (var m : register) : register is
block {
patch m with map [
("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address) -> (4,9);
("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" : address) -> (1,2)
]
} with m

Removing Bindings

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

PascaLIGO features a special syntactic construct to remove bindings from maps, of the form remove <key> from map <map>. For example,

function rem (var m : register) : register is
block {
remove ("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address) from map moves
} with m
const updated_map : register = rem (moves)