Skip to main content
Version: Next

Polymorphism

LIGO supports simple polymorphism when introducing declarations. This allows to write functions parametric on a type that can be later instantiated to concrete types.

The identity function

For any given type t, there is a canonical function of type t -> t (function from t to t): it takes an argument, and returns it immediately. For instance, we can write the identity function for int as follows:

const id = (x: int): int => x;

However, if we would want to use the same function on a different type, such as nat, we will need to write a new definition:

const idnat = (x : nat): nat => x;

If we read carefully, we see that there is almost no difference between id and idnat: it is just the type that changes, but for the rest, the body of the function remains the same.

Thanks to parametric polymorphism, we can write a single function declaration that works for both cases.

const id = <T>(x : T) : T => x;

Here T is a type variable which can be generalised. In general, types prefixed with _ are treated as generalisable.

We can then use this function directly in different types by just regular application:

const three_i : int = id(3);
const three_s : string = id("three");

During compilation, LIGO will monomorphise the polymorphic functions into specific instances, resulting in Michelson code that does not contain polymorphic function declarations anymore.

Polymorphism with parametric types

Polymorphism is especially useful when writing functions over parametric types, which include built-in types like lists, sets, and maps.

As an example, we will see how to implement list reversing parametrically on any type, rather than just on lists of a specific type.

Similar to the id example, we can introduce a type variable that can be generalised. We will write a direct version of the function using an accumulator, but the reader can experiment with different variations by using List combinators.

function rev <T>(xs : list<T>) : list<T> {
const rev = <T>([xs, acc] : [list<T>, list<T>]) : list<T> =>
match(xs) {
when([]): acc;
when([y,...ys]): rev([ys, [y,...acc]])
};
return rev([xs, ([] as list<T>)]);
};

We use an accumulator variable acc to keep the elements of the list processed, consing each element on it. As with the identity function, we can then use it directly in different types:

const lint : list<int> = rev([1, 2, 3]);
const lnat : list<nat> = rev([1n, 2n, 3n]);