Skip to main content
Version: Next

Namespaces

Namespaces are collections of related definitions that make them modular and help them avoid name collisions. A common example of a namespace is a data type and associated operations over it, such as stacks or queues. A namespace can contain the declaration of a type that represents a currency with functions that manipulate the currency and constants that apply to it. Other pieces of code can access these definitions, providing maintainability, reusability and safety.

note

Developers often put a single smart contract in a namespace, but LIGO does not require you to put a contract in a namespace and it does not limit namespaces to contain a single contract.

Namespaces have some similarities with records because they can both contain multiple definitions. However, there are significant differences between records and namespaces:

  • Records are expressions and therefore can be used as values, and namespaces are not expressions and can't be used as values. For example, you can pass a record as an argument to a function, but you cannot pass a namespace in this way except in specific circumstances, such using the contract_of function to create a contract from a namespace to use in Testing.

  • Records cannot package type and value definitions together like namespaces can.

Which construct you use depends on your design and strategy: namespaces behave like libraries and records behave like individual units of computation.

Creating namespaces

To create a namespace, declare it with the keyword namespace and a name that starts with a capital letter. Then put the type, function, value, and nested namespace definitions in a block opened by "{" and closed by "}"

For example, the following code defines a namespace named Euro to represent the Euro currency. It packages together a type (internally called t), an operation add that sums two values of the given currency, and constants for one and two Euros.

// This is euro.jsligo
namespace Euro {
export type t = nat;
export const add = (a: t, b: t) : t => a + b;
export const one: t = 1n;
export const two: t = 2n;
};

To access the contents of a namespace, use the name of the namespace and the selection operator ".", as with records. For example, this piece of code in the same file defines a value of the Euro type and uses the functions and constants in the namespace to manipulate it:

type euro_balance = Euro.t;
const add_tip = (s: euro_balance): euro_balance =>
Euro.add(s, Euro.one);

In principle, you could change the internal implementation of the Euro namespace without having to change the euro_balance type or the add_tip function. For example, if you decide to support manipulating negative values, you could change the Euro.t type to an integer:

namespace Euro {
export type t = int;
export const add = (a: t, b: t) : t => a + b;
export const sub = (a: t, b: t) : t => a - b;
export const one: t = 1;
export const two: t = 2;
};

The add_tip function still works, and no change is needed. Abstraction accomplished!

However, clients that use the Euro namespace might still break the abstraction if they directly use the underlying representation of Euro.t. For example, the type Euro.t is a transparent alias of nat (or int). In this case, other code might break if it performs operations that are valid on nats but not on integers. Client code should always try to respect the interface provided by the namespace and not make assumptions on its current underlying representation.

Nesting namespaces

You can define a namespace inside another namespace. For example, this version of the Euro namespace groups constants in a sub-namespace:

namespace Euro {
export type t = nat;
export let add = (a: t, b: t): t => a + b;
export namespace Coin {
export let one: t = 1n;
export let two: t = 2n;
};
};

To access nested namespaces, use the selection operator as many times as necessary:

type euro_balance = Euro.t;
const increment = (s: euro_balance): euro_balance =>
Euro.add(s, Euro.Coin.one);

Note that the sub-namespace Coin must be prefixed by the keyword export to make its contents available outside the namespace.

Aliasing namespaces

You can apply an alias to a namespace to create namespaces that work as synonyms of previously defined namespaces. Creating a synonym of a namespace can be useful to implement a namespace that is currently the same as another namespace for now but may need to change in the future. For example, until 2025, the Bulgarian Lev is pegged to the euro currency, so the Bulgarian_Lev namespace is an alias of the Euro namespace:

namespace Euro {
export type t = nat;
export const add = (a: t, b: t) : t => a + b;
export const one: t = 1n;
export const two: t = 2n;
};
import Bulgarian_Lev = Euro;

Now other code can use the Lev just like it is a Euro for now, and you can change how the Lev works later.

note

You must use the import keyword to alias the namespace, even if it is in the same file.

You can import namespaces from other files with the #import directive, but only if the namespaces have the @public decorator. See #import.