Solidity — Part 5- Functions, Call & Return Parameters
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.