Tutorial for Liquidity

TODO

Tuples

Tuples in Liquidity are compiled to nested pairs in Michelson:

(x, y, z) <=> Pair x (Pair y z)

Tuples can be accessed using the field access notation of Liquidity:

let t = (x,y,z) in
let should_be_true = t.(2) = z in

A new tuple can be created from another one using the field access update notation of Liquidity:

let t = (1,2,3) in
let z = t.(2) <- 4 in

Tuples can be deconstructed:

(* t : (int * (bool * nat) * int) *)
let _, (b, _), i = t in
...
(* b : bool
   i : int *)

Records

Record types can be declared and used inside a liquidity contract:

type storage = {
  x : string;
  y : int;
}

Such types can be created and used inside programs:

let r = { x = "foo"; y = 3 } in
r.x

Records are compiled as tuples.

Deep record creation is possible using the notation:

let r1 = { x = 1; y = { z = 3 } } in
let r2 = r1.y.z <- 4 in
...

Variants

Variants should be defined before use, before the contract declaration:

type t =
| X
| Y of int
| Z of string * nat

Variants can be created using:

let x = X 3 in
let y = Z s in
...

The match construct can be used to pattern-match on them, but only on the first constructor:

match x with
| X -> ...
| Y i -> ...
| Z s -> ...

where i and s are variables that are bound by the construct to the parameter of the variant.

Parameters of variants can also be deconstructed when they are tuples, so one can write:

match x with
| X -> ...
| Y i -> ...
| Z (s, n) -> ...

A special case of variants is the Left | Right predefined variant, called variant:

type (``left, ``right) variant =
| Left of ``left
| Right of ``right

Occurrences of these variants may optionally be constrained with type annotations:

let x = (Left 3 : (int, string) variant) in
match x with
| Left left  -> ...
| Right right -> ...

Another special variant is the Source variant: it is used to refer to the contract that called the current contract:

let s = (Source : (unit, unit) contract) in
...

As for Left and Right, Source occurrences may be constrained by type annotations.

Functions and Closures

Unlike Michelson, functions in Liquidity can also be closures. They can take multiple arguments and are curryfied. Because closures are lambda-lifted, it is however recommended to use a single tuple argument when possible. Arguments and return types are inferred, which enables to write polymorphic functions. Type annotations may optionally be used to constrain their type.

Function applications are written by juxtaposing the function and its argument:

let succ x = x + 1 in
let one = succ 0 in
...

but they can also be written using the Lambda.pipe function or the |> operator:

let succ = fun x -> x + 1 in
let one = 0 |> succ in
...

A toplevel function can also be defined before the main entry point:

[%%version 0.2]

let succ x = x + 1

let%entry main ... =
  ...
  let one = succ 0 in
  ...

Closures can be created with the same syntax:

let p = 10 in
let sum_and_add_p x y = x + y + p in
let r = sum_and_add_p 3 4 in
...

This is equivalent to:

let p = 10 in
let sum_and_add_p =
  fun x ->
    fun y ->
      x + y + p
in
let r = (sum_and_add_p 3) 4 in
...

Functions with multiple arguments should take a tuple as argument because curried versions will generate larger code and should be avoided unless partial application is important. The previous function should be written as:

let sum_and_add_p (x, y) =
  let p = 10 in
  x + y + p
in
let r = sum_and_add_p (3, 4) in
...

Also note that polymorphic functions will be monomorphised in Michelson, which will duplicate the function for each of its type and thus generate larger code.

Loops

Loops in liquidity share some syntax with functions, but the body of the loop is not a function, so it can access the environment, as would a closure do:

let end_loop = 5 in
let x = Loop.loop (fun x ->
    ...
    (x < end_loop, x')
  ) x_init
in
...

As shown in this example, the body of the loop returns a pair, whose first part is the condition to remain in the loop, and the second part is the accumulator.