Full Guide to Understanding Functions in Solidity (Experience + Tips)

Full Guide to Understanding Functions in Solidity (Experience + Tips)

Photo by Surface on Unsplash

Introduction

Solidity is the most popularly adopted language for writing smart contracts among EVM-compatible chains. Anyone who wants to be proficient at writing Solidity smart contracts for several use cases needs to understand an important concept — functions.

Functions form the larger part of most smart contracts in Solidity. They help define business logic and several other important terms.

Whether you are a complete beginner or an intermediate smart contract developer, I wrote this blog post to teach you how you can confidently define functions yourself.

I will start from the fundamentals, then teach you how to do more fantastic operations with functions. That leads us to the question, what is a function?

What is a Solidity function?

A function in Solidity is a block of code that generally starts with the keyword function and outlines the modality for executing an action.

Let me explain with a couple of real-world scenarios:

Why do you use your phone? I guess one of the main reasons is to receive calls. Then communication is one of the functions of your phone.

You cannot enter your house with your door locked unless you have the key or remote, then the function of those items is to give you access to your house.

Do you get the idea? Now, let’s talk code:

function sendOrder(string memory burgerMenu, uint quantity) public payable{
          require(customer == msg.sender, "Only the customers can call this function");

          orderseq++;

          //We passed the needed members of the struct as params so we can call them
          orders[orderseq] = Order (orderseq, burgerMenu, quantity, 0, 0, 0, 0, true);
      }

The purpose of the function above is to send an order. It is a public function, and it can receive ether due to adding the payable keyword.

It has a require statement that only a customer can be the msg.sender, which is the address that makes a call. Then it passes some details into an array once a customer creates an order.

Don’t worry if you didn’t understand what was playing out here. The main point I want you to know is that this function helps customers of a platform create an order.

Can you see how we use functions in smart contracts?

What is the syntactical format of a Solidity function?

There is a format for writing a Solidity function. This is it:

function theFunctionName(insert initial parameters, which is sometimes called arguments) visibility mutability payability modifiers (returning parameter){
    // input your code here
}

The Solidity compiler will only recognize a function as one if and only if it starts with the function keyword. This is something you must always bear in mind as a developer.

Then you must name your Solidity functions following the Camel Casing Convention. It must start with a lower case, then other words can start with capital letters, and there must be no space in between.

Examples include theFunctionName instead of TheFunctionName.

Moving on, you can define parameters if you need them as local variables within your function. Here is what I mean:

Let us use the earlier function as an example:

      function sendOrder(string memory burgerMenu, uint quantity) public payable{
          require(customer == msg.sender, "Only the customers can call this function");

          orderseq++;

          // we passed the needed members of the struct as params so we can call them
          orders[orderseq] = Order (orderseq, burgerMenu, quantity, 0, 0, 0, 0, true);
      }

When we were done, we realized burgerMenu and quantity were not recognized locally, hence the reason we defined them as parameters after naming our function.

You can include some other details like visibility, mutability, payability, and modifiers after the parameters. You can also have a returning parameter if you write a getter function.

A returning parameter is the data type or the detail you want a function to send back once you call it. Consider this example:

      function getInvoice(uint invoiceID) public view returns (address customer, uint orderNo, uint invoice_date) {
          require(invoices[invoiceID].created, "The invoice doesn't exist");

          Invoice storage invoice = invoices[invoiceID];
          Order storage order = orders[_invoice.orderNo];

          return(customer, order.ID, order.orderDate);
      }

The function above has these returning parameters: address customer, uint orderNo, uint invoice_date. When you call this function, you can get these details—which are the returning parameters—about a customer.

Free Functions and Contract-based Functions

As a developer, you can write functions in two major locations, either within or outside a contract. This is an example of functions written inside a contract:

contract Mentation {
    uint public times;

    function increase() public {
        times +=1;
    }

    function decrease() public {
        times -= 1;
    }

    function getTimes() public view returns(uint) {
        return times;
    }
}

We wrote functions increase, decrease, and getTimes within a contract named Mentation.

Let us examine the one written outside of a contract:

  function myBalance()  pure returns (uint) {
      return address(this).balance;
  }

We didn’t write myBalance within a contract.

Hold this in your left palm: Functions declared outside a contract are called Free Functions.

You might wonder, “How do free and contract-based functions operate? Are there any differences?”

There is really no glaring difference between a free function and a contract-based one. But there are a few ones on a closer look.

First, free contracts do not enjoy the benefits of leveraging state variables, which are variables created outside of any function in a contract.

Secondly, you cannot use the keyword this in them, as in address(this).balance.

Understanding Function Visibility in Solidity

You might have seen a couple of cases where developers marked some contracts as external, public, private, or internal. Those keywords specify the extent to which other contracts can interact with those functions.

The more you write smart contracts, the more you will understand how to classify functions based on their sensitivity or use. There is no brick-and-mortar rule on this; you, the developer, are in charge, and everything is at your discretion.

We will examine the four possible forms of visibility in Solidity:

Public

Public functions have an open design. They are visible to every other contract to call. This is an example:

    function increase() public {
        times +=1;
        address(this).balance = msg.sender;
    }

Private

Private functions are like shaving sticks; you don’t share them with others. Once a developer declares a function private, it will not be callable whenever other contracts inherit it. The reason is simple: it is private to the actual contract where it was defined.

    function decrease() private {
        times -= 1;
    }

External

You cannot call external functions within a contract. External functions are declared to be called by other contracts. Hence, the name external.

  function myBalance() external view returns (uint) {
      return address(this).balance;
  }

Internal

Internal functions can only be accessed within the contract they are created or an inheriting contract. The difference between internal and private functions is that the latter remains invisible even in inheriting contracts.

  function addRegulators (address regulator) internal {
      require(msg.sender == owner, "Only the owner ");
      // so you can pass addresses
      regulator = regulator;
  }

About Getter Functions and Their Mutability Specification

Functions with returning parameters are getter functions. These functions can often alter the current data or condition—better put as the state—of a smart contract.

As a developer, you are in charge of deciding which getter function can affect the mutability of the state. We have two major mutability-related specifications.

View

A function marked with view can read and alter the state of the smart contract. It can update the current data, or even the logic. For instance, a function that sends ether or emits an event has automatically updated the state.

I will tell you this: Use this keyword for functions you feel are important for the state and can interact safely.

Pure

Generally, pure functions can neither read nor update the state. That is, they do not have the capabilities of view functions. But from my knowledge of the Ethereum Virtual Machine, the ability of a pure function not to read the state is unrealistic.

This is why I said that:

Functions can read the state of a contract at the level of the EVM, and the fact that they are marked pure or not does not even count at all. In essence, the main point of difference of pure functions is that they cannot update the state.

The Role of Function Modifiers in Solidity

Function modifiers are helpful whenever you want to add some conditions to one or more functions so that only those who meet those conditions can call them. Another reason we developers use them is to avoid any repetition of logic.

For example, if a condition applies to one or more functions, we can bundle the conditions into a modifier and simply add it when writing the functions. Does that ring a bell? Yes, it follows the DRY Principle in engineering!

There is a syntax for modifiers:

modifier theName {
        require(

        );
        _;
    }

Declare a modifier with its keyword modifier, name it, then add a require statement. Lock it with a _; once you are done, as it shows that the modification statement or instruction has ended.

There is no better way for you to understand than when I show you the code.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;
  contract Flight{

      struct Seat {
      bytes32 uuid;
      address owner;
      address passenger;
      uint price;
      }
    Seat[] public seats;

    mapping (address => uint[]) public ownerSeats;
    mapping (address => uint) public passengerSeat;

  modifier hasTicket() {
    require(ownerSeats[msg.sender].length > 0, "Must have a ticket in the first place");
    ;
  }

  function transferSeat (uint seatIndex, address transferTo) public hasTicket {
    require(seats[_seatIndex].owner == msg.sender, "Someone else owns this seat");

    seats[seatIndex].passenger = transferTo;
    passengerSeat[_transferTo] = _seatIndex;       
  }

}

Check the contract above well, you will notice the transferSeat function has a modifier we marked hasTicket. The implication of this modifier on the function is to make this function available only to those who have tickets.

The Two Forms of Fallback Function

Earlier, I explained that we always mark all functions with the function keyword. That was a general statement or rule, and I am sure you know how every rule always has an exception.

This leads me to talk about fallback functions. A fallback function is a rare class of functions with a static method of declaration other than including the function keyword. We have two types: a plain fallback function and receive fallback.

Use a plain fallback function to handle cases where anyone tries to call an undefined function in a contract. In those cases, the plain fallback function will take in the data. This is how you can write a plain fallback function:

fallback () external payable{ }

By the way, I have discovered that fallback functions do not fall within the radar of what you can define as free functions. You must declare them within the contracts.

Moving on to receive ether function. As its name says, declare it to accept tokens into your contract.

receive () external payable{}

There is one important thing you must bear in mind: Even though receive () external payable{ } is the best for receiving ether, fallback () external payable{ } can also receive in worst-case scenarios. However, it has a short gas limit for doing that as it is not its main work.

What is function overloading? How does Solidity handle it?

Function overloading occurs when two Solidity functions have the same name, even though they have different initial parameters.

In your job as a Solidity developer, you can land into a situation called function overloading. Even if you don’t do it intentionally, you can run into it because of an inherited codebase.

Let me show you an example:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

contract OTC {
    function placeTrade(uint amount) public pure returns (uint out) {
        out = amount;
    }

    function placeTrade(uint amount, bool polar) public pure returns (uint publish) {
        if (polar)
            publish = amount;
    }
}

placeTrade is an overloaded function because two functions bear the name. How does Solidity resolve it?

It starts with the initial parameters in each function: they must be implicitly convertible. The main point here is there must be a difference between the parameters of these functions.

What next?

So far, I feel that is all you need to know about functions. Subsequently, I can take you more tutorials on how you can write smart contracts for various use cases, such as storing your important document or keeping your pension.

While we are at it, you should learn about how to build an ERC-20 token. I like feedback a lot; if you really enjoyed this tutorial, tweet about it on Twitter and tag me — @jofawole.

Keep building and having fun!