ligo/gitlab-pages/docs/language-basics/sets-lists-tuples.md

20 KiB

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

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

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 (x,y) to store the coordinates x and 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, so, for instance, (x,y) has always a different type from (x,y,z), whereas (y,x) might have the same type as (x,y).

Like records, tuple components can be of arbitrary types.

Defining Tuples

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

let full_name : full_name = ("Alice", "Johnson") // Optional parentheses
type full_name = (string, string);  // Alias

let full_name : full_name = ("Alice", "Johnson");

Accessing Components

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

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 Lists

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

const larger_list : list (int) = 5 # my_list // [5;1;2;2]

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.

let larger_list : int list = 5 :: my_list // [5;1;2;2]

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.

let larger_list : list (int) = [5, ...my_list]; // [5,1,2,2]

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

Functional Iteration over Lists

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

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.

In PascaLIGO, the predefined functional iterator implementing the iterated operation over lists is called list_iter.

In the following example, a list is iterated to check that all its elements (integers) are greater than 3:

function iter_op (const l : list (int)) : unit is
  block {
    function iterated (const i : int) : unit is
      if i > 2 then Unit else (failwith ("Below range.") : unit)
  } with list_iter (iterated, l)

The iterated function must be pure, that is, it cannot mutate variables.

In CameLIGO, the predefined functional iterator implementing the iterated operation over lists is called List.iter.

In the following example, a list is iterated to check that all its elements (integers) are greater than 3:

let iter_op (l : int list) : unit =
  let predicate = fun (i : int) -> assert (i > 3)
  in List.iter predicate l

In ReasonLIGO, the predefined functional iterator implementing the iterated operation over lists is called List.iter.

In the following example, a list is iterated to check that all its elements (integers) are greater than 3:

let iter_op = (l : list (int)) : unit => {
  let predicate = (i : int) => assert (i > 3);
  List.iter (predicate, l);
};

Map Operation

We may want to change all the elements of a given list by applying to them a function. This is called a map operation, not to be confused with the map data structure.

In PascaLIGO, the predefined functional iterator implementing the map operation over lists is called list_map and is used as follows:

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)

In CameLIGO, the predefined functional iterator implementing the map operation over lists is called List.map and is used as follows:

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

In ReasonLIGO, the predefined functional iterator implementing the map operation over lists is called List.map and is used as follows:

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

Fold Operation

A fold 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.

In PascaLIGO, the predefined functional iterator implementing the fold operation over lists is called list_fold and is used as follows:

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

const sum_of_elements : int = list_fold (sum, my_list, 0)

The folded function must be pure, that is, it cannot mutate variables.

In CameLIGO, the predefined functional iterator implementing the fold operation over lists is called List.fold and is used as follows:

let sum (acc, i: int * int) : int = acc + i
let sum_of_elements : int = List.fold sum my_list 0

In ReasonLIGO, the predefined functional iterator implementing the fold operation over lists is called List.fold and is used as follows:

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

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 []

In CameLIGO, the empty set is denoted by the predefined value Set.empty.

let my_set : int set = Set.empty

In CameLIGO, the empty set is denoted by the predefined value Set.empty.

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

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 contains that operates like an infix operator checking membership in a set.

const contains_3 : bool = my_set contains 3

In CameLIGO, the predefined predicate Set.mem tests for membership in a set as follows:

let contains_3 : bool = Set.mem 3 my_set

In ReasonLIGO, the predefined predicate Set.mem tests for membership in a set as follows:

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

Cardinal

In PascaLIGO, the predefined function size returns the number of elements in a given set as follows:

const set_size : nat = size (my_set)

In CameLIGO, the predefined function Set.size returns the number of elements in a given set as follows:

let set_size : nat = Set.size my_set

In ReasonLIGO, the predefined function Set.size returns the number of elements in a given set as follows:

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

Updating Sets

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

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

Functional Iteration over Sets

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

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.

In PascaLIGO, the predefined functional iterator implementing the iterated operation over sets is called set_iter.

In the following example, a set is iterated to check that all its elements (integers) are greater than 3:

function iter_op (const s : set (int)) : unit is
  block {
    function iterated (const i : int) : unit is
      if i > 2 then Unit else (failwith ("Below range.") : unit)
  } with set_iter (iterated, s)

The iterated function must be pure, that is, it cannot mutate variables.

In CameLIGO, the predefined functional iterator implementing the iterated operation over sets is called Set.iter.

In the following example, a set is iterated to check that all its elements (integers) are greater than 3:

let iter_op (s : int set) : unit =
  let predicate = fun (i : int) -> assert (i > 3)
  in Set.iter predicate s

In ReasonLIGO, the predefined functional iterator implementing the iterated operation over sets is called Set.iter.

In the following example, a set is iterated to check that all its elements (integers) are greater than 3:

let iter_op = (s : set (int)) : unit => {
  let predicate = (i : int) => assert (i > 3);
  Set.iter (predicate, s);
};

Map Operation

We may want to change all the elements of a given set by applying to them a function. This is called a map operation, not to be confused with the map data structure.

In PascaLIGO, the predefined functional iterator implementing the map operation over sets is called set_map and is used as follows:

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

// Creates a new set with all elements incremented by 1
const plus_one : set (int) = set_map (increment, larger_set)

In CameLIGO, the predefined functional iterator implementing the map operation over sets is called Set.map and is used as follows:

let increment (i : int) : int = i + 1

// Creates a new set with all elements incremented by 1
let plus_one : int set = Set.map increment larger_set

In ReasonLIGO, the predefined functional iterator implementing the map operation over sets is called Set.map and is used as follows:

let increment = (i : int) : int => i + 1;

// Creates a new set with all elements incremented by 1
let plus_one : set (int) = Set.map (increment, larger_set);

Fold Operation

A fold 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.

In PascaLIGO, the predefined functional iterator implementing the fold operation over sets is called set_fold and is used as follows:

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

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

The folded function must be pure, that is, it cannot mutate variables.

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