Solidity Functions — Everything You Need to Know About Types
function (<parameter types>) {internal|external|public|private} [pure|constant|view|payable] [(modifiers)] [returns (<return types>)]
Function Types
As we venture forward on our mission to learn Solidity, we must again clarify an important concept: Function Types. Sometimes referred to as keywords, restrictors, or state mutabilities, many developers simply use the type to describe it. For instance, they will call the function a view function, pure function, etc. This may seem easy to understand but they often get confused with function visibilities to a point where we aren’t sure which is which.
The key to keeping it all straight is to remember that visibilities define the “where” for functions, specifically, from where functions can be invoked. Function types tell the “what”, what functions can do. For someone with a Java background, like myself, this is a nice feature. Visibilities, referred to as access modifiers in Java, are available in Java function definitions but function types are not. This makes it easier for developers to tell the compiler, the EVM, and the world what their function is capable of.
A Brief Divergence
To understand function types, we must first understand Solidity variables. The type of function required is directly related to what variables it uses and how it uses them, so let’s briefly discuss variable categories. I use the word categories because it’s in reference to how variables are stored(their location) as opposed to what data type they are. It has nothing to do with the typing of the data itself, ie uint256, int256, addr, etc. It’s about where the data lives. We must concern ourselves with how the EVM is handling/storing them. For the sake of function types, we need to concern ourselves with these three variable categories: state(storage), local(memory), and global.
State variables are defined outside of functions(preferably at the top of the contract) and are used to permanently store data. They may be accessed from functions but “live” outside of them. These variables are persisted in storage and will exist for as long as the contract exists.
Local variables are ones that are defined within a function. They are only “alive” while the function is running. When the function is invoked, this could be from within the same contract, another contract, or from an externally owned account(EOA), the variable is created in memory. While the function runs, it can be interacted with as needed, and then it is no longer viable once the function exits. They are held in memory.
Global variables “live” in a special namespace and are used to get general blockchain and transaction data. block.number, msg.sender, tx.gasprice, and now are a few examples. These always exist in the global namespace. Mainly, they are used to give information about the blockchain or are common utility functions. The Java equivalent would be JNDI names or JVM Environment variables. They are held in the global namespace.
Now that we have that business taken care of, let’s talk about how it affects function types.
TYPES
view
We begin with view. These functions can NOT modify state. This means they can’t change any variable defined outside of function, referred to as storage variables. They can read them, modify them, etc., but are not allowed to change them anyway. This is not only true for the function itself, but also for any function that it uses. view functions are only allowed to invoke other view functions or pure functions.
Additionally, they may only use certain keywords and cost no gas if they’re called externally or internally by another view function. Once again, because they aren’t changing state.
A good example of a view function is a getter. They touch the state variable in order to serve the data back to the requestor but never change it.
address ownerAddress;
// Get the address of the owner
function getOwner() public view returns (address) {
return ownerAddress;
}
Note: In older versions of Solidity, constant was used in place of view. See below.
pure
pure functions are easy to remember. The pure essentially means “pure computation”. They are even more restrictive than their view counterparts as they can’t even access state variables like view functions. They can only deal with local variables and access other pure functions. Examples of pure functions would be cryptography or mathematical operations.
function calculateSum() public pure returns(uint sum){
uint a = 1;
uint b = 2;
return a + b;
}
calculateSum() only uses memory variables, ones defined within the function itself. Keep in mind that if this function had used a state variable in any way, including calling a separate function not marked as pure, it would not compile.
payable
It is debatable whether payable is a function type or a modifier. A simple search will reveal many different articles which refer to it both ways. I prefer to think of it as a function type because it can’t be specified with view or pure whereas user-defined modifiers can. When you try to add it to a view function in Remix, it gives the error, ‘State mutability already specified as “view”.’ Adding a modifier to view or pure functions is common practice. That’s enough evidence to satisfy me.
payable makes a function capable of sending and receiving Ether. It explicitly tells the EVM and the world that the function is intended to process Ether in some way. This allows developers to handle the Ether so it isn’t locked in the contract forever. The Ether arrives as msg.value and it can then be processed as necessary. This processing might be transferring the amount to another, storing it in the contract, burning it, etc.
Trying to send Ether to a function not marked as payable causes the transaction to fail, however, sending 0 Ether to a payable function does not automatically fail. This condition needs to be tested within the function if it could pose a problem. Additionally, multiple functions can be marked as payable.
An example could be a deposit function that allows 1 Ether to be deposited at a time for any address:
function deposit() payable external {
// deposit sizes are restricted to 1 Ether
require(msg.value == 1 ether);
// add to their balance
balances[msg.sender] += msg.value;
}
What about nothing? The absence of view/pure
When neither view nor pure is specified, it means the function may modify state. If the function actually does not modify a state variable, the compiler should give a suggestion to add view to the definition. It’s good practice to follow that suggestion because it clearly defines the intention of the function and it can save gas. The same principle holds true for pure.
constant
Last, and rightly so, are constant functions. They are the old version of what we now know as view functions. They are considered an alias of view functions and are a throwback to when Solidity was still finding its identity. If you see one in older versions of Solidity, just know that they can’t modify state variables. They were dropped in Solidity 0.5.0.
Up Next
In an upcoming installment, I’ll be discussing function modifiers and special functions.