Modules¶
A module is a set of function definitions and variable declarations which enables code reuse. Vyper favors code reuse through composition, rather than inheritance. A module encapsulates everything needed for code reuse, from type and function declarations to state. It is important to note that functions which make use of defined state must be initialized in order to use that state, whereas functions that are “pure” do not require this.
Declaring and using modules¶
The simplest way to define a module is to write a contract. In Vyper, any contract is a valid module! For example, the following contract is also a valid module.
# ownable.vy
owner: address
@deploy
def __init__():
self.owner = msg.sender
def _check_owner():
assert self.owner == msg.sender
@pure
def _times_two(x: uint256) -> uint256:
return x * 2
@external
def update_owner(new_owner: address):
self._check_owner()
self.owner = new_owner
This contract basically has two bits of functionality which can be reused upon import, the _check_owner()
function and the update_owner()
function. The _check_owner()
is an internal function which can be used as a helper to check ownership in importing modules, while the update_owner()
is an external function which an importing module can itself export as an externally facing piece of functionality.
You can use this module’s functionality simply by importing it, however any functionality that you do not use from a module will not be included in the final compilation target. For example, if you don’t use the initializes
statement to declare a module’s location in the storage layout, you cannot use its state. Similarly, if you don’t explicitly export
an external function from a module, it will not appear in the runtime code.
Importing a module¶
A module can be imported using import
or from ... import
statements. The following are all equivalent ways to import the above module:
import ownable # accessible as `ownable`
import ownable as ow # accessible as `ow`
from . import ownable # accessible as `ownable`
from . import ownable as ow # accessible as `ow`
When importing using the as
keyword, the module will be referred to by its alias in the rest of the contract.
The _times_two()
helper function in the above module can be immediately used without any further work since it is “pure” and doesn’t depend on initialized state.
import ownable as helper
@external
def my_function(x: uint256) -> uint256:
return helper._times_two(x)
The other functions cannot be used yet, because they touch the ownable
module’s state. There are two ways to declare a module so that its state can be used.
Initializing a module¶
In order to use a module’s state, it must be “initialized”. A module can be initialized with the initializes
keyword. This declares the module’s location in the contract’s Storage Layout. It also creates a requirement to invoke the module’s __init__() function, if it has one. This is a well-formedness requirement, since it does not make sense to access a module’s state unless its __init__()
function has been called.
import ownable
initializes: ownable
@deploy
def __init__():
ownable.__init__()
@external
def my_access_controlled_function():
ownable._check_owner() # reverts unless msg.sender == ownable.owner
... # do things that only the owner can do
It is a compile-time error to invoke a module’s __init__()
function more than once!
A module’s state can be directly accessed just by prefixing the name of a variable with the module’s alias, like follows:
@external
def get_owner() -> address:
return ownable.owner
The uses
statement¶
Another way of using a contract’s state without directly initializing it is to use the uses
keyword. This is a more advanced usage which is expected to be mostly utilized by library designers. The uses
statement allows a module to use another module’s state but defer its initialization to another module in the compilation tree (most likely a user of the library in question).
This is best illustrated with an example:
# ownable_2step.vy
import ownable
uses: ownable
# does not export ownable.transfer_ownership!
pending_owner: address # the pending owner in the 2-step transfer process
@deploy
def __init__():
self.pending_owner = empty(address)
@external
def begin_transfer(new_owner: address):
ownable._check_owner()
self.pending_owner = new_owner
@external
def accept_transfer():
assert msg.sender == self.pending_owner
ownable.owner = new_owner
self.pending_owner = empty(address)
Here, the ownable_2step
module does not want to seal off access to calling the ownable
module’s __init__()
function. So, it utilizes the uses: ownable
statement to get access to the ownable
module’s state, without the requirement to initialize it. Note that this is a valid module, but it is not a valid contract (that is, it cannot produce bytecode) because it does not initialize the ownable
module. To make a valid contract, the user of the ownable_2step
module would be responsible for initializing the ownable
module themselves (as in the next section: initializing dependencies).
Whether to use
or initialize
a module is a choice which is left up to the library designer.
Technical notes on the design¶
This section contains some notes on the design from a language design perspective. It can be safely skipped if you are just interested in how to use modules, and not necessarily in programming language theory.
The design of the module system takes inspiration from (but is not directly related to) the rust language’s borrow checker. In the language of type systems, module initialization is modeled as an affine constraint which is promoted to a linear constraint if the module’s state is touched in the compilation target. In practice, what this means is:
A module must be “used” or “initialized” before its state can be accessed in an import
A module may be “used” many times
A module which is “used” or its state touched must be “initialized” exactly once
To read more about the design background of Vyper’s module system, please see its original design document.
Initializing a module with dependencies¶
Sometimes, you may encounter a module which itself uses
other modules. Vyper’s module system is designed to allow this, but it requires you make explicit the access to the imported module’s state. The above ownable_2step.vy
contract is an example of this. If you wanted to initialize the ownable_2step
module, it would use the special :=
(aka “walrus”) syntax, and look something like this:
import ownable
import ownable_2step
initializes: ownable
# ownable is explicitly declared as a state dependency of `ownable_2step`
initializes: ownable_2step[ownable := ownable]
@deploy
def __init__():
ownable.__init__()
ownable_2step.__init__()
# export all external functions from ownable_2step
exports: ownable_2step.__interface__
Warning
In normal usage, you should make sure that __init__()
functions are called in dependency order. In the above example, you can get unexpected behavior if ownable_2step.__init__()
is called before ownable.__init__()
! The compiler may enforce this behavior in the future.
Exporting functions¶
In Vyper, @external
functions are not automatically exposed (i.e., included in the runtime code) in the importing contract. This is a safety feature, it means that any externally facing functionality must be explicitly defined in the top-level of the compilation target.
So, exporting external functions from modules is accomplished using the exports
keyword. In Vyper, functions can be exported individually, or, a wholesale export of all the functions in an interface can be done. The special interface module.__interface__
is a compiler-defined interface, which automatically includes all the functions in a module.
The following are all ways of exporting functions from an imported module.
# export a single function from `ownable_2step`
exports: ownable_2step.transfer_ownership
# export multiple functions from `ownable_2step`, being explicit about
# which specific functions are being exported
exports: (
ownable_2step.transfer_ownership,
ownable_2step.accept_ownership,
)
# export all IERC20 functions from `base_token`
exports: base_token.IERC20
# export all external functions from `ownable_2step`
exports: ownable_2step.__interface__
Note
Any exported interfaces must be implemented by the module. For example, in the above example, base_token
must contain implements: IERC20
, or else the compiler will raise an error.