--- id: maps-records title: Records and Maps --- import Syntax from '@theme/Syntax'; 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 and example of record type declaration. ```pascaligo group=records1 type user is record [ id : nat; is_admin : bool; name : string ] ``` ```cameligo group=records1 type user = { id : nat; is_admin : bool; name : string } ``` ```reasonligo group=records1 type user = { id : nat, is_admin : bool, name : string }; ``` And here is how a record value is defined: ```pascaligo group=records1 const alice : user = record [ id = 1n; is_admin = True; name = "Alice" ] ``` ```cameligo group=records1 let alice : user = { id = 1n; is_admin = true; name = "Alice" } ``` ```reasonligo group=records1 let alice : user = { 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: ```pascaligo group=records1 const alice_admin : bool = alice.is_admin ``` ```cameligo group=records1 let alice_admin : bool = alice.is_admin ``` ```reasonligo group=records1 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. In PascaLIGO, the shape of that expression is ` with `. The record variable is the record to update and the record value is the update itself. ```pascaligo group=records2 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: ```shell ligo run-function gitlab-pages/docs/language-basics/src/maps-records/record_update.ligo 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 namless new version of it has been created and returned by the blockless function. The syntax for the functional updates of record in CameLIGO follows that of OCaml: ```cameligo group=records2 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 with 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: ```shell ligo run-function gitlab-pages/docs/language-basics/src/maps-records/record_update.mligo 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. The syntax for the functional updates of record in ReasonLIGO follows that of ReasonML: ```reasonligo group=records2 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: ```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. ### 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 afterwards 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. ```pascaligo group=records3 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 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: ```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 ```pascaligo group=records4 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 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: ```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: ```pascaligo group=records5 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 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: ```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. ```pascaligo group=maps type move is int * int type register is map (address, move) ``` ```cameligo group=maps type move = int * int type register = (address, move) map ``` ```reasonligo group=maps type move = (int, int); type register = map (address, move); ``` ### Creating an Empty Map Here is how to create an empty map. ```pascaligo group=maps const empty : register = map [] ``` ```cameligo group=maps let empty : register = Map.empty ``` ```reasonligo group=maps let empty : register = Map.empty ``` ### Creating a Non-empty Map And here is how to create a non-empty map value: ```pascaligo group=maps 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 `("" : address)` means that we cast a string into an address. Also, `map` is a keyword. ```cameligo group=maps let moves : register = Map.literal [ (("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" : address), (1,2)); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address), (0,3))] ``` The `Map.literal` predefined function builds a map from a list of key-value pair tuples, `(, )`. Note also the `;` to separate individual map entries. `("": address)` means that we type-cast a string into an address. --> ```reasonligo group=maps let moves : register = Map.literal ([ ("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" : address, (1,2)), ("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address, (0,3))]); ``` The `Map.literal` predefined function builds a map from a list of key-value pair tuples, `(, )`. Note also the `;` to separate individual map entries. `("": address)` means that we type-cast a string into an address. --> ### 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: ```pascaligo group=maps const my_balance : option (move) = moves [("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address)] ``` ```cameligo group=maps let my_balance : move option = Map.find_opt ("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address) moves ``` ```reasonligo group=maps let my_balance : option (move) = Map.find_opt (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : 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*. ```pascaligo group=maps function force_access (const key : address; const moves : register) : move is case moves[key] of Some (move) -> move | None -> (failwith ("No move.") : move) end ``` ```cameligo group=maps let force_access (key, moves : address * register) : move = match Map.find_opt key moves with Some move -> move | None -> (failwith "No move." : move) ``` ```reasonligo group=maps let force_access = ((key, moves) : (address, register)) : move => { switch (Map.find_opt (key, moves)) { | Some (move) => move | None => failwith ("No move.") : 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*. The values of a PascaLIGO map can be updated using the usual assignment syntax `[] := `. Let us consider an example. ```pascaligo group=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. ```pascaligo group=maps 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. We can update a binding in a map in CameLIGO by means of the `Map.update` built-in function: ```cameligo group=maps let assign (m : register) : register = Map.update ("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address) (Some (4,9)) m ``` Notice the optional value `Some (4,9)` instead of `(4,9)`. If we had use `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. ```cameligo group=maps let add (m : register) : register = Map.add ("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address) (4,9) m ``` We can update a binding in a map in ReasonLIGO by means of the `Map.update` built-in function: ```reasonligo group=maps let assign = (m : register) : register => Map.update (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address), Some ((4,9)), m); ``` Notice the optional value `Some (4,9)` instead of `(4,9)`. If we had use `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. ```reasonligo group=maps let add = (m : register) : register => Map.add (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address), (4,9), m); ``` 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. ```pascaligo group=maps function delete (const key : address; var moves : register) : register is block { remove key from map moves } with moves ``` In CameLIGO, we use the predefined function `Map.remove` as follows: ```cameligo group=maps let delete (key, moves : address * register) : register = Map.remove key moves ``` In ReasonLIGO, we use the predefined function `Map.remove` as follows: ```reasonligo group=maps 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`. ```pascaligo group=maps 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*. ```cameligo group=maps let iter_op (m : register) : unit = let predicate = fun (i,j : address * move) -> assert (j.0 > 3) in Map.iter predicate m ``` ```reasonligo group=maps let iter_op = (m : register) : unit => { let predicate = ((i,j) : (address, move)) => 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. ```pascaligo group=maps 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*. ```cameligo group=maps let map_op (m : register) : register = let increment = fun (i,j : address * move) -> j.0, j.1 + 1 in Map.map increment m ``` ```reasonligo group=maps let map_op = (m : register) : register => { let increment = ((i,j): (address, move)) => (j[0], j[1] + 1); 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. ```pascaligo group=maps 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*. ```cameligo group=maps let fold_op (m : register) : int = let folded = fun (i,j : int * (address * move)) -> i + j.1.1 in Map.fold folded m 5 ``` ```reasonligo group=maps let fold_op = (m : register) : int => { let folded = ((i,j): (int, (address, move))) => i + j[1][1]; 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: ```pascaligo group=big_maps type move is int * int type register is big_map (address, move) ``` ```cameligo group=big_maps type move = int * int type register = (address, move) big_map ``` ```reasonligo group=big_maps type move = (int, int); type register = big_map (address, move); ``` ### Creating an Empty Big Map Here is how to create an empty big map. ```pascaligo group=big_maps const empty : register = big_map [] ``` ```cameligo group=big_maps let empty : register = Big_map.empty ``` ```reasonligo group=big_maps let empty : register = Big_map.empty ``` ### Creating a Non-empty Map And here is how to create a non-empty map value: ```pascaligo group=big_maps 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 `("" : address)` means that we cast a string into an address. --> ```cameligo group=big_maps let moves : register = Big_map.literal [ (("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" : address), (1,2)); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address), (0,3))] ``` The predefind function `Big_map.literal` constructs a big map from a list of key-value pairs `(, )`. Note also the semicolon separating individual map entries. The annotated value `(" value>" : address)` means that we cast a string into an address. ```reasonligo group=big_maps let moves : register = Big_map.literal ([ ("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" : address, (1,2)), ("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address, (0,3))]); ``` The predefind function `Big_map.literal` constructs a big map from a list of key-value pairs `(, )`. Note also the semicolon separating individual map entries. The annotated value `(" 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: ```pascaligo group=big_maps const my_balance : option (move) = moves [("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address)] ``` ```cameligo group=big_maps let my_balance : move option = Big_map.find_opt ("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address) moves ``` ```reasonligo group=big_maps let my_balance : option (move) = Big_map.find_opt ("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address, moves); ``` ### Updating Big Maps The values of a PascaLIGO big map can be updated using the assignment syntax for ordinary maps ```pascaligo group=big_maps function add (var m : register) : register is block { m [("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address)] := (4,9) } with m const updated_map : register = add (moves) ``` We can update a big map in CameLIGO using the `Big_map.update` built-in: ```cameligo group=big_maps let updated_map : register = Big_map.update ("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" : address) (Some (4,9)) moves ``` We can update a big map in ReasonLIGO using the `Big_map.update` built-in: ```reasonligo group=big_maps let updated_map : register = Big_map.update (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address), Some ((4,9)), moves); ``` ### 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 from map `. For example, ```pascaligo group=big_maps function rem (var m : register) : register is block { remove ("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address) from map moves } with m const updated_map : register = rem (moves) ``` In CameLIGO, the predefined function which removes a binding in a map is called `Map.remove` and is used as follows: ```cameligo group=big_maps let updated_map : register = Map.remove ("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address) moves ``` In ReasonLIGO, the predefined function which removes a binding in a map is called `Map.remove` and is used as follows: ```reasonligo group=big_maps let updated_map : register = Map.remove (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address), moves) ```