Solidity — Part 5- Functions, Call & Return Parameters

Shishir Singh
7 min readJul 6, 2023

--

Solidity is a programming language used to write smart contracts on the Ethereum blockchain. Smart contracts are self-executing contracts that allow for the automatic transfer of digital assets when certain conditions are met. In this series, we will cover some of the more tricky areas of Solidity, aimed at the Intermediate level of Solidity skills.

In Part 4 we covered Transfer, Send, and Call.

In this article, we delve into the nuances of function visibility and data locations that can be used to control access to functions and manage how data is stored and manipulated.

Function Visibility

In Solidity, function visibility is declared in four types: public, external, internal, and private. The visibility type determines how and where a function can be called from.

Public Functions

Public functions form part of the contract interface and can be called both internally and externally. This means they can be invoked from within the same contract, from derived contracts, or from other contracts and transactions.

contract MyContract {
uint public data;

function setData(uint a) public {
data = a;
}
}

In the above example, the setData function is declared as public, meaning it can be called both internally and externally.

Use public when you want a function to be part of the contract interface, accessible both internally and externally, including from derived contracts.

External Functions

External functions are also part of the contract interface but can only be called externally, i.e., from other contracts or transactions. They cannot be called internally within the same contract (i.e., this.f() works, but f() does not).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MyContract {
uint public data;

function setData(uint _data) external {
data = _data;
}

function increaseData(uint _increment) public {
// This will work because we're using 'this.' to make an external function call
this.setData(data + _increment);
}

function doubleData() public {
// This will NOT work because we're trying to make an internal function call to an external function
// Uncommenting the following line will cause the contract to fail to compile
// setData(data * 2);
}
}

In this example, setData is an external function that sets the value of the data state variable. The increaseData function correctly calls setData using this.setData, which is an external call. The doubleData function attempts to call setData without this., which would be an internal call. However, because setData is an external function, this is not allowed, and the line is commented out to prevent a compilation error.

Use external when a function is part of the contract interface and is intended to be called only from other contracts or transactions, not internally, which can save gas when large amounts of data are involved.

Internal Functions

Internal functions can only be called from within the current contract or contracts deriving from it. They cannot be accessed externally. This is the default visibility level for functions if no visibility specifier is given.

contract MyContract {
function internalFunc() internal {
// function body
}
}

In this example, internalFunc is an internal function and can only be called from within the current contract or contracts deriving from it.

Use internal when a function should only be accessible within the contract it is defined in and from derived contracts, providing a way to share common logic between functions while keeping it hidden from the outside world.

Private Functions

Private functions are similar to internal functions but are not accessible in derived contracts. They can only be accessed within the contract they are defined in.

contract MyContract {
function privateFunc() private {
// function body
}
}

In this example, privateFunc is a private function and can only be accessed within the contract it is defined in.

Use private when a function should only be accessible within the contract it is defined in, providing the highest level of encapsulation and security for sensitive operations that should not be exposed to derived contracts or external callers.

Function Modifiers

In addition to visibility specifiers, Solidity provides function modifiers to specify the behavior of functions. The view and pure modifiers are particularly important when designing smart contracts.

View Functions

A function can be declared as view if it does not modify the state. In other words, a view function promises not to modify the state of the contract, its variables, or the Ethereum blockchain in any way. It can only read the state, not change it.solidityCopy code

contract MyContract {
uint public data = 10;
function getData() public view returns (uint) {
return data;
}
}

In this example, getData is a view function. It returns the value of the state variable data but does not modify it.

Pure Functions

A pure function promises not to read from or write to the contract state. This means it cannot access or modify state variables and cannot call other functions that do so.

contract MyContract {
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
}

In this example, add is a pure function. It performs Function Inputs and Outputs.

Return Values

Functions in Solidity can return multiple values and accept various data types as inputs or outputs. However, public functions cannot accept certain data types, such as mappings, as inputs or outputs.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Function {
// Functions can return multiple values.
function returnMany() public pure returns (uint, bool, uint) {
return (1, true, 2);
}
// Return values can be assigned to their name.
// In this case the return statement can be omitted.
function assigned() public pure returns (uint x, bool b, uint y) {
x = 1;
b = true;
y = 2;
return (x, b, y);
}
// Use destructuring assignment when calling another
// function that returns multiple values.
function destructuringAssignments()
public
pure
returns (uint, bool, uint, uint, uint)
{
(uint i, bool b, uint j) = returnMany();
// Values can be left out.
(uint x, , uint y) = (4, 5, 6);
return (i, b, j, x, y);
}
// Cannot use map for either input or output
// Can use array for input
function arrayInput(uint[] memory _arr) public {}
// Can use array for output
uint[] public arr;
function arrayOutput() public view returns (uint[] memory) {
return arr;
}
}

In the above example, the returnMany function returns multiple values. The named function shows that return values can be named. The assigned function demonstrates that return values can be assigned to their name, and in such cases, the return statement can be omitted. The destructuringAssignments function shows how to use destructuring assignment when calling another function that returns multiple values.

The arrayInput function shows that arrays can be used for input, but mappings cannot. The arrayOutput function shows that arrays can also be used for output.

Function Calls with Key-Value Inputs

In Solidity, you can call a function with key-value inputs. This is particularly useful when a function has many inputs.

contract XYZ {
function someFuncWithManyInputs(
uint x,
uint y,
uint z,
address a,
bool b,
string memory c
) public pure returns (uint) {}

function callFunc() external pure returns (uint) {
return someFuncWithManyInputs(1, 2, 3, address(0), true, "c");
}

function callFuncWithKeyValue() external pure returns (uint) {
return someFuncWithManyInputs({a: address(0), b: true, c: "c", x: 1, y: 2, z: 3});
}
}

In the above example, the someFuncWithManyInputs function has many inputs. The callFunc function calls someFuncWithManyInputs with positional arguments. The callFuncWithKeyValue function calls someFuncWithManyInputs with key-value arguments, which can be easier to read and understand.

Data Locations: Storage, Memory, and Calldata

In Solidity, variables are declared as either storage, memory, or calldata. These keywords represent data locations that indicate where the data is stored and how it behaves when manipulated.

Storage

The storage data location is the default location for state variables, which are permanently stored on the blockchain. This means that values stored in storage persist across function calls and transactions.

contract SimpleStorage {
uint storedData; // state variable defaults to storage
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}

In the above example, storedData is a state variable that defaults to storage. The set function sets the value of storedData, and the get function returns its value.

Memory

The memory data location is used for temporary variables. It is erased between (external) function calls and is cheaper to use than storage.

contract MemoryExample {
function f() public pure {
uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 2;
x[2] = 3;
}
}

In the above example, x is a dynamic array that is stored in memory. It is initialized with a length of 3, and its elements are set to 1, 2, and 3.

Calldata

The calldata data location is used for function parameters of external functions. It is similar to memory but is read-only.

contract CalldataExample {
function f(uint[] calldata x) external pure {
// function body
}
}

In the above example, x is an array that is stored in calldata. It is passed as a parameter to the external function f.

Combining Function Visibility, Modifiers, and Data Locations

Function visibility, modifiers, and data locations can be combined in various ways to achieve different behaviors in Solidity. Here is an example of a contract that uses all of these concepts:

contract ComplexExample {
uint[] public data;
function addData(uint x) public {
data.push(x);
}
function getData(uint index) public view returns (uint) {
return data[index];
}
function calculateSum(uint[] memory arr) public pure returns (uint) {
uint sum = 0;
for (uint i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
}

In this contract, data is a public state variable stored in storage. The addData function is a public function that adds an element to data. The getData function is a public view function that returns an element from data at a given index. The calculateSum function is a public pure function that calculates the sum of an array in memory.

Conclusion

Function visibility specifiers (public, private, internal, and external) control how and where a function can be called from, providing a way to restrict access and enhance security. Function modifiers (view and pure) provide additional information about the behavior of a function, allowing for better optimization and error checking.

Data locations (storage, memory, and calldata) determine where data is stored and how it behaves when manipulated. Storage is used for state variables that persist across function calls and transactions, memory is used for temporary variables that are erased between function calls, and calldata is used for function parameters of external functions and is read-only.

In Part 6 we cover In Progress.

--

--

Shishir Singh

Digital Assets, Blockchains, DLTs, Tokenization & Protocols & AI Intersection