ligo/gitlab-pages/docs/language-basics/sets-lists-tuples.md
2020-02-06 13:34:32 +01:00

13 KiB

id title
sets-lists-tuples Tuples, Lists, Sets

Apart from complex data types such as maps and records, ligo also exposes tuples, lists and sets.

⚠️ Make sure to pick the appropriate data type for your use case, and bear in mind the related gas costs.

Tuples

Tuples gather a given number of values in a specific order and those values, called components, can be retrieved by their index (position). Probably the most common tuple is the pair. For example, if we were storing coordinates on a two dimensional grid we might use a pair of type int * int to store the coordinates x and y as the pair value (x,y). There is a specific order, so (y,x) is not equal to (x,y). The number of components is part of the type of a tuple, so, for example, we cannot add an extra component to a pair and obtain a triple of the same type: (x,y) has always a different type from (x,y,z), whereas (y,x) may have the same type.

Like records, tuple components can be of arbitrary types.

Defining a tuple

Unlike a record, tuple types do not have to be defined before they can be used. However below we will give them names by type aliasing.

type full_name is string * string  // Alias

const full_name : full_name = ("Alice", "Johnson")
type full_name = string * string  // Alias

(* The parenthesis here are optional *)
let full_name : full_name = ("Alice", "Johnson")
type full_name = (string, string);  // Alias

(* The parenthesis here are optional *)
let full_name : full_name = ("Alice", "Johnson");

Accessing an Element in a Tuple

Accessing the components of a tuple in OCaml is achieved by pattern matching. LIGO currently supports tuple patterns only in the parameters of functions, not in pattern matching. In LIGO, however, we can access components by their position in their tuple, which cannot be done in OCaml.

Tuple components are one-indexed like so:

const first_name : string = full_name.1;

Tuple elements are zero-indexed and accessed like so:

let first_name : string = full_name.0

Tuple components are one-indexed like so:

let first_name : string = full_name[1];

Lists

Lists are linear collections of elements of the same type. Linear means that, in order to reach an element in a list, we must visit all the elements before (sequential access). Elements can be repeated, as only their order in the collection matters. The first element is called the head, and the sub-list after the head is called the tail. For those familiar with algorithmic data structure, you can think of a list a stack, where the top is written on the left.

💡 Lists are useful when returning operations from a smart contract's entrypoint.

Defining a List

const my_list : list (int) = list [1; 2; 2] // The head is 1
let my_list : int list = [1; 2; 2] // The head is 1
let my_list : list (int) = [1, 2, 2]; // The head is 1

Adding to a List

Lists can be augmented by adding an element before the head (or, in terms of stack, by pushing an element on top). This operation is usually called consing in functional languages.

In PascaLIGO, the cons operator is infix and noted #. It is not symmetric: on the left lies the element to cons, and, on the right, a list on which to cons. (The symbol is helpfully asymmetric to remind you of that.)

In CameLIGO, the cons operator is infix and noted ::. It is not symmetric: on the left lies the element to cons, and, on the right, a list on which to cons.

In ReasonLIGO, the cons operator is infix and noted , .... It is not symmetric: on the left lies the element to cons, and, on the right, a list on which to cons.

const larger_list : list (int) = 5 # my_list
let larger_list : int list = 5 :: my_list
let larger_list : list (int) = [5, ...my_list];

💡 Lists can be iterated, folded or mapped to different values. You can find additional examples here and other built-in operators here

Mapping of a List

We may want to apply a function to all the elements of a list and obtain the resulting list, in the same order. For example, we may want to create a list that contains all the elements of another list incremented by one. This is a special case of fold operation called a map operation. Map operations (not to be confused by the map data structure), are predefined functions in LIGO. They take as a parameter the function to apply to all the elements. Of course, that function must return a value of the same type as the element.

In PascaLIGO, the map function is called list_map.

In CameLIGO and ReasonLIGO, the map function is called List.map.

function increment (const i : int): int is i + 1

// Creates a new list with all elements incremented by 1
const plus_one : list (int) = list_map (increment, larger_list)
let increment (i : int) : int = i + 1

// Creates a new list with all elements incremented by 1
let plus_one : int list = List.map increment larger_list
let increment = (i : int) : int => i + 1;

// Creates a new list with all elements incremented by 1
let plus_one : list (int) = List.map (increment, larger_list);

Folding of over a List

function sum (const acc : int; const i : int): int is acc + i

const sum_of_elements : int = list_fold (sum, my_list, 0)
let sum (acc, i: int * int) : int = acc + i
let sum_of_elements : int = List.fold sum my_list 0
let sum = ((result, i): (int, int)): int => result + i;
let sum_of_elements : int = List.fold (sum, my_list, 0);

Sets

Sets are unordered collections of values of the same type, like lists are ordered collections. Like the mathematical sets and lists, sets can be empty and, if not, elements of sets in LIGO are unique, whereas they can be repeated in a list.

Empty Sets

const my_set : set (int) = set []
let my_set : int set = (Set.empty : int set)
let my_set : set (int) = (Set.empty : set (int));

Non-empty Sets

In PascaLIGO, the notation for sets is similar to that for lists, except the keyword set is used before:

const my_set : set (int) = set [3; 2; 2; 1]

You can check that 2 is not repeated in my_set by using the LIGO compiler like this (the output will sort the elements of the set, but that order is not significant for the compiler):

ligo evaluate-value
gitlab-pages/docs/language-basics/src/sets-lists-tuples/sets.ligo my_set
# Outputs: { 3 ; 2 ; 1 }

In CameLIGO, there is no predefined syntactic construct for sets: you must build your set by adding to the empty set. (This is the way in OCaml.)

let my_set : int set =
  Set.add 3 (Set.add 2 (Set.add 2 (Set.add 1 (Set.empty : int set))))

You can check that 2 is not repeated in my_set by using the LIGO compiler like this (the output will sort the elements of the set, but that order is not significant for the compiler):

ligo evaluate-value
gitlab-pages/docs/language-basics/src/sets-lists-tuples/sets.mligo my_set
# Outputs: { 3 ; 2 ; 1 }

In ReasonLIGO, there is no predefined syntactic construct for sets: you must build your set by adding to the empty set. (This is the way in OCaml.)

let my_set : set (int) =
  Set.add (3, Set.add (2, Set.add (2, Set.add (1, Set.empty : set (int)))));

You can check that 2 is not repeated in my_set by using the LIGO compiler like this (the output will sort the elements of the set, but that order is not significant for the compiler):

ligo evaluate-value
gitlab-pages/docs/language-basics/src/sets-lists-tuples/sets.religo my_set
# Outputs: { 3 ; 2 ; 1 }

Set Membership

PascaLIGO features a special keyword constains that operates like an infix operator checking membership in a set.

const contains_3 : bool = my_set contains 3
let contains_3 : bool = Set.mem 3 my_set
let contains_3 : bool = Set.mem (3, my_set);

Cardinal

const set_size : nat = size (my_set)
let set_size : nat = Set.size my_set
let set_size : nat = Set.size (my_set);

Adding or Removing from a Set

In PascaLIGO, there are two ways to update a set. Either we create a new set from the given one, or we modify it in-place. First, let us consider the former:

const larger_set  : set (int) = set_add (4, my_set)

const smaller_set : set (int) = set_remove (3, my_set)

If we are in a block, we can use an instruction to modify the set bound to a given variable. This is called a patch. It is only possible to add elements by means of a patch, not remove any: it is the union of two sets.

In the following example, the parameter set s of function update is augmented (as the with s shows) to include 4 and 7, that is, this instruction is equivalent to perform the union of two sets, one that is modified in-place, and the other given as a literal (extensional definition).

function update (var s : set (int)) : set (int) is block {
  patch s with set [4; 7]
} with s

const new_set : set (int) = update (my_set)

In CameLIGO, we update a given set by creating another one, with or without some elements.

let larger_set  : int set = Set.add 4 my_set

let smaller_set : int set = Set.remove 3 my_set

In ReasonLIGO, we update a given set by creating another one, with or without some elements.

let larger_set  : set (int) = Set.add (4, my_set);

let smaller_set : set (int) = Set.remove (3, my_set);

Folding over a Set

Given a set, we may want to apply a function in turn to all the elements it contains, while accumulating some value which is returned at the end. This is a fold operation. In the following example, we sum up all the elements of the set my_set defined above.

In PascaLIGO, the folded function takes the accumulator first and the (current) set element second. The predefined fold is called set_fold.

function sum (const acc : int; const i : int): int is acc + i

const sum_of_elements : int = set_fold (sum, my_set, 0)

It is possible to use a loop over a set as well.

function loop (const s : set (int)) : int is block {
  var sum : int := 0;
  for element in set s block {
    sum := sum + element
  }
} with sum

In CameLIGO, the predefined fold over sets is called Set.fold.

let sum (acc, i : int * int) : int = acc + i

let sum_of_elements : int = Set.fold sum my_set 0

In ReasonLIGO, the predefined fold over sets is called Set.fold.

let sum = ((acc, i) : (int, int)) : int => acc + i;

let sum_of_elements : int = Set.fold (sum, my_set, 0);