Welcome back to Vyper for Beginners! This lesson is dedicated to functions.
Vyper for Beginners Series
- Part 1 — Introduction
- Part 2 — Toolkit
- Part 3 — Variables, Flow Control, Functions, Hello World!
- Part 4 — Interfaces
- Part 5 — Dynamic Arrays
- Part 6 — Structs and Mappings
- Part 7 — Functions
Functions
Functions are fundamental to programming. A function is a series of operations applied, in order, to a piece or pieces of data. Programmers should be familiar with the built-in functions of their programming language. As a program becomes more complex, programmers are encouraged to remember the phrase “never repeat yourself”. If you need to apply some operations to a set of data once, it is OK to include it inline in your main code block. However if you find yourself applying that same set of operations again in a different place, it’s in your interest to create a separate function for these operations, then reference this function whenever you need to repeat those operations.
In addition to being a container for reusable code, a smart contract function often represents a means for users and other contracts to interact. The familiar ERC-20 token actions transfer and approve are simply functions that are executed by the associate token’s smart contract.
A function often has a set of inputs and outputs, but either can be omitted to suit the function being written.
Vyper Function Requirements
Vyper requires the following when declaring a function:
- A function must be declared with a visibility decorator
- The function’s inputs and output must be declared with their associated data types
- All return values must match the output data type
- The functions must be declared in an order that avoids forward references (one function calling another that has not been defined yet)
Decorators
A decorator is a keyword with an @ symbol, placed above a function definition. The “decorator” description is taken directly from Python. In Python, a decorator allows a function to be “wrapped” by another function. This “wrapper” function modifies the behavior of the original function without having to directly modify its code.
I’m unfamiliar with the mechanism behind Vyper’s decorator keywords, but it provides the same behavior here. Adding one or more decorators allows you to restrict a function to execute only under certain conditions.
There are two visibility decorators that control who may initiate (call) the function:
@internal
— the function is only accessible from within another function defined inside this contract@external
— the function is only accessible from outside the contract
There are four mutability decorators that control other aspects of the function:
@pure
— the function does not perform any reads of the contract state or any variables defined within@view
— the function may perform reads of the contract state and variables, but does not modify the state@payable
— the function may receive Ether (or the equivalent native token)@nonreentrant(<unique_key>)
— this function, and all other functions that use the same key, may not be called externally more than once during the same transaction
Wherever @payable
is omitted, @nonpayable
is assumed, however specifying it does no harm and increases clarity.
Return Values
A function that returns a value must include that return value type in its definition.
Consider a function that finds a user’s balance. It is likely that uint256
is an appropriate return value type for this function.
A function can return any data type (value or reference), except for a mapping.
If a function does not return anything, the return
statement may be omitted. If the function returns a value, a return
statement must appear somewhere.
Calling Internal Functions
An internal function can only be called from within another function, and they are all accessed from the self
object.
To execute an internal function called internal_test()
, include the function call self.internal_test()
inside another function.
Modifiers
Solidity developers are familiar with the concept of a modifier. A modifier is a short function that requires some condition to evaluate to True
. This modifier name can be added to another function declaration. This behaves similarly to the decorators described above. A common example is the modifier onlyOwner
which will execute a short check that checks this on each call of a function that includes onlyOwner
in its function declaration:
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
require(owner() == _msgSender(), "Ownable: caller is not the owner");
}
Vyper does not provide this feature for security reasons (it is more difficult to analyze what a function will do if is has a modifier).
However the same behavior could be easily implemented in Vyper by writing an internal function that asserts the same requirement:
@internal
def _checkOwner(addr: address):
assert addr == self.owner, "NOT OWNER"
@external
def admin_function():
self._checkOwner()
[...]
Constructor
The constructor is a special function that is intended to be executed once. It can be used to set the initial values for storage and immutable variables. A common use for the constructor is to set a contract owner by storing the msg.sender
value permanently.
A Vyper constructor is defined using the special __init__()
function name. It must be marked @external
and may not return any value. Here is a simple constructor that stores the contract owner’s address in two places.
# @version ^0.3
owner: address
OWNER: immutable(address)
@external
def __init__():
self.owner = msg.sender
OWNER = msg.sender
Vyper allows you to record some values permanently in the deployed bytecode instead of consuming a storage slot. This results in a cheaper deployment gas cost for variables that will never change.
Fallback Function
By default Vyper will construct a fallback function that automatically reverts. This is useful as a mechanism to fail when a function cannot be found that matches the calldata. The fallback function is defined using the __default__()
name.
If you want your function to be always reject basic Ether transfers, as well as unknown functions, define it as such:
@external
def __default__():
raise
This “fail-proof” fallback will prevent users from sending Ether to the contract by mistake.