In Solidity, we have 3 categories of functions:
  • Functions that modify something on the blockchain – we can call them write functions.
  • Functions that read data from the blockchain, but that don’t modify anything – we can call them read functions. Those functions are labeled with the “view” keyword.
  • And finally, we have functions that neither read nor write from the blockchain. Those functions are marked with the “pure” keyword and they are often used to perform some calculations without accessing any state variables.
State changing functions:

So, a function that is labeled as view or pure is not allowed to change the state of the blockchain. Here is a list of actions and statements that are considered to modify the state of the blockchain:

  • Modifying a smart contract state variable.
  • Emitting an event. When we emit an event, we are writing some data to the transaction log that is associated with our contract address and therefore, we are modifying the blockchain.
  • Creating a contract using the new keyword. Of course, whenever we create a new smart contract, we are storing its bytecode on the blockchain and therefore we are changing the overall state of the blockchain.
  • Using the “selfdestruct” statement to destroy our smart contract. Again, the bytecode of the contract will be deleted and the overall state of the blockchain changes.
  • Sending Ether. When we send Ether, we are crediting one address and debiting another, which of course modifies the state of the blockchain.
  • Calling another function that modifies state.
Pure functions:

Let’s also take a quick look at the special case of pure functions that are not allowed to access or read any blockchain data. Here are the actions and statements that are considered to read state and that are not allowed in pure functions:

  • Of course, reading any contract state variables.
  • Accessing the balance member of an address. This could be: someAddress.balance or the balance of the smart contract: address(this).balance.
  • Accessing any of the members of the global variables block, tx or msg – however, you are allowed to call the data member of the msg variable (msg.data).
  • Calling another that is not marked as pure.

So, we basically have 3 categories of functions in Solidity, but what’s really important is if a function modifies the blockchain or not. Modifying and non-modifying functions are treated in a completely different way by the EVM (Ethereum Virtual Machine).

Blockchain modifying functions (write functions):
  • Require a transaction that needs to be signed with a private key.
  • A fee needs to be paid. The amount of the fee depends on the complexity of the function to be executed (amount of code and type of opcodes) and the current state of the network (congestion of the network).
  • The effects of calling a state changing function are not immediately visible on the blockchain. The transaction first needs to be added to a new block, which can take from 10 seconds to several hours, depending on the fee, the user is willing to pay.
Functions that don’t modify the blockchain (view and pure functions):
  • Do not require a transaction – a simple function call is sufficient.
  • Function calls are free, no gas fee needs to be paid.
  • the result of the function call is returned immediately.
Function visibility modifiers:

We also need to take a quick look at the visibility of functions in Solidity. This is similar to other high-level programming languages, but there are a few minor differences. In Solidity, we have 4 different visibility modifiers for functions: private, internal, external and public.

  • Private: Only accessible by other functions defined in the same contract. Private functions are not visible on derived contracts.
  • Internal: Visible on derived contracts and can only be accessed internally by other contract functions => an internal function cannot be called by an external client application.
  • External: Can only be called externally => an external function cannot be called by another contract function within the same contract.
  • Public: Can be internally and externally.

Most high-level languages provide visibility modifiers like: private, public and internal (using a different name for internal, eg: protected…). What’s different in Solidity is the external visibility modifier. But, what’s the point, why not just use public instead?

As you already know, in Solidity we need to pay a fee to execute a state changing function. Using an external instead of a public function allows us to save a little bit of gas. So, it’s a good idea to use an external function whenever we are sure that we won’t need to call that function internally from our smart contract. In other words, the external modifier allows us to optimize our contract in terms of gas consumption.

Sending Ether to a smart contract function:

Solidity allows us to send Ether when we are calling a contract function. An example would be a banking smart contract that allows us to deposit and withdraw funds. On calling the deposit function, the user would send along a certain amount of Ether that would then be credited to his account.

However, we can’t just send ETH to any function. The function must be marked with the “payable” keyword in order to receive Ether, otherwise the function will revert. You can then retrieve the amount that was sent along using “msg.value” in your payable function.

Return parameters of Solidity functions:

Before we take a look at a code example, let’s finish off discussing return parameters. In Solidity, we use the “returns” keyword in the function declaration and in brackets we list the argument types and optionally the argument names.

If you don’t provide argument names with the returns statement, you need to define corresponding data types in the function body and return those variables.

Also, you can either assign values to the variables defined in the returns statement and then leave the function without using a return… statement. Or, you provide one or more values (depending on the number of arguments provided in the returns statement) with the return statement in the function body.

To review what we just learned, let’s take a look at the following code example:

 

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.17;

contract Functions {
    uint public value;
    
    function changeValue(uint _value) public {
        value = _value;
    }

    //if changeValue is external, we can't call it from here (except if we call it using the "this" keyword)
    function callChangeValue(uint _value) external {
        _value++;
        changeValue(_value);
    }

    //simple view function => cannot change state
    function multiplyValue(uint _value) external view returns (uint) {
        return value * _value;
    }

    //simple pure function => cannot read from state
    function addAndMultiply(uint _valueA, uint _valueB) public pure returns(uint, uint) {
        uint add = _valueA + _valueB;
        uint mult = _valueA * _valueB;
        return (add, mult);

        //or, provide names for the return parameters and set the value of those variables,
        //without using the return statement. eg: ...(uint addResult, uint multResult)        
        //addResult = _valueA + _valueB;
        //multResult = _valueA * _valueB;
    }    

    function callAddAndMultiply(uint _valueA, uint _valueB) public pure returns(uint) {
        (uint add, uint multi) = addAndMultiply(_valueA, _valueB);
        return add + multi;
    }

    event ReceivedETH(address indexed sender, uint amount);

    function depositETH() external payable {
        emit ReceivedETH(msg.sender, msg.value);
    }
}

The code in the contract isn’t doing anything useful, it is just to demonstrate the various features of functions in Solidity.

We start by declaring a public state variable (value) and the compiler will automatically generate a public getter function that allows us to retrieve the value of our state variable. By the way, the default visibility for state variables is internal, which does not generate a getter function.

The next function (changeValue) is a public function that allows us to modify the value of our state variable. Functions on the other hand don’t have a default visibility. You need to specify the visibility explicitly, otherwise the compiler generates an error.

We also define an external function (callChangeValue). The external function can call other non-external contract functions, but it cannot be called by any other contract functions.

The next function is of type “view” (multiplyValue). It cannot modify state, but it is allowed to read state.

Then, we have a pure function (addAndMultiply) that can neither write nor read from state. This function also demonstrates how you can return multiple values. You can either assign variables of the corresponding data type and return them with a return statement, or, you can assign the variables listed in the returns statement and leave the function without using a return statement.

The next function (callAddAndMultiply) shows how you can assign multiple values that are returned from another function.

The final function (depositETH) is a payable function that can receive Ether. If you try to send ETH to a non-payable function, you will get an error message and the function will revert.