Skip to main content
Version: 1.0.0

Modules

Note that in JsLIGO modules are called namespaces.

Modules are a programming language construction that allows us to package related definitions together. A canonical example of a module is a data type and associated operations over it (e.g. stacks or queues). The rest of the program can access these definitions in a regular and abstract way, providing maintainability, reusability and safety.

For a concrete example, we could create a module that packages a type that represents amounts in a particular currency together with functions that manipulate these amounts: constants, addition, subtraction, etc. A piece of code that uses this module can be agnostic concerning how the type is actually represented inside the module: it's abstract.

Declaring Modules

Modules are introduced using the namespace keyword. For example, the following code defines a module EURO that packages together a type, called t, together with an operation add that sums two values of the given currency, as well as constants for zero and one.

namespace EURO {
export type t = nat;
export const add = (a: t, b: t): t => a + b;
export const zero: t = 0n;
export const one: t = 1n
}

In this example you will also notice the export keyword. A statement within a module can be accessed from outside the module if it is exported.

Using Modules

We can access a module's components by using the . operator. Let's suppose that our storage keeps a value in euros using the previously defined module EURO. Then, we can write a main entry point that increments the storage value each time it is called.

type storage = EURO.t;
@entry
let main = (_action: unit, store: storage): [list<operation>, storage] =>
[list([]), EURO.add(store, EURO.one)];

In principle, we could change the implementation of EURO, without having to change the storage type or the function main. For example, if we decide later that we should support manipulating negative values, we could change EURO as follows:

namespace EURO {
export type t = int;
export const add = (a: t, b: t): t => a + b;
export const zero: t = 0;
export const one: t = 1;
}

Notice that the code in main still works, and no change is needed. Abstraction accomplished!

⚠️ Please note that code using the module EURO might still break the abstraction if it directly uses the underlying representation of EURO.t. Client code should always try to respect the interface provided by the module, and not make assumptions on its current underlying representation (e.g. EURO.t is a transparent alias of nat; future versons of LIGO might make this an opaque / abstract type).

Nested Modules: Sub-Modules

Modules can be nested, which means that we can define a module inside another module. Let's see how that works, and define a variant of EURO in which the constants are all grouped inside using a sub-module.

namespace EURO {
export type t = nat;
export let add = (a: t, b: t): t => a + b;
export namespace CONST {
export let zero: t = 0n;
export let one: t = 1n;
};
};

To access nested modules we simply apply the accessor operator more than once:

type storage = EURO.t;
@entry
let main = (_action: unit, store: storage) : [list<operation>, storage] =>
[list([]), EURO.add(store, EURO.CONST.one)]

Modules and Imports: Build System

Modules also allow us to separate our code in different files: when we import a file, we obtain a module encapsulating all the definitions in it. This will become very handy for organising large contracts, as we can divide it into different files, and the module system keeps the naming space clean.

Generally, we will take a set of definitions that can be naturally grouped by functionality, and put them together in a separate file.

For example, in JsLIGO, we can create a file imported.jsligo:

export type t = nat;
export const add = (a: t, b: t): t => a + b;
export const zero: t = 0n;
export const one: t = 1n;

Later, in another file, we can import imported.jsligo as a module, and use its definitions. For example, we could create a importer.jsligo that imports all definitions from imported.jsligo as the module EURO:

#import "./gitlab-pages/docs/language-basics/src/modules/imported.jsligo" "EURO"
type storage = EURO.t;
@entry
const main = (_action: unit, store: storage): [list<operation>, storage] =>
[list([]), EURO.add(store, EURO.one)];

We can compile the file that uses the #import statement directly, without having to mention the imported file.

ligo compile contract gitlab-pages/docs/language-basics/src/modules/importer.jsligo

Module Aliases

LIGO supports module aliases, that is, modules that work as synonyms to other (previously defined) modules. This feature can be useful if we could implement a module using a previously defined one, but in the future, we might need to change it.

import US_DOLLAR = EURO;

Modules as Contracts

When a module contains declarations that are tagged with the attribute @entry (called @entry decorator in JsLIGO), then a contract can be obtained from such module. All declarations in the module tagged as @entry are grouped, and a dispatcher contract is generated.

namespace C {
@entry
const increment = (p : int, s : int) : [list<operation>, int] => [list([]), s + p];
@entry
const decrement = (p : int, s : int) : [list<operation>, int] => [list([]), s - p];
};

A module can be compiled as a contract using -m:

ligo compile contract gitlab-pages/docs/language-basics/src/modules/contract.jsligo -m C

To access the contract from the module, the primitive contract_of can be used. The type of the parameter generated for the module can be obtaining using the primitive parameter_of. This is particularly useful for working with the testing framework, in conjunction with the function Test.originate:

const test = do {
let orig = Test.originate(contract_of(C), 0, 0tez);
Test.transfer_exn(orig.addr, (Increment (42)), 1mutez);
return assert(Test.get_storage(orig.addr) == 42);
};