Solidity Function Type Examples – A Simple Illustrated Guide

In this article, we’ll continue our story on function types, but this time, we’ll go through several illustrative examples and learn how they work and breathe in practice.

This way, by complementing what we already know about function types, we’ll get a broader perspective and better understanding.

It’s part of our long-standing tradition to make this (and other) articles a faithful companion, or a supplement to the official Solidity documentation, starting with these docs for this article’s topics.

  • In the next section, we’ll just lay out the source code for easier reference and testing purposes.
  • In the subsequent section, we’ll analyze what the code does and comment on the points of interest.

Smart Contract – Internal Function Type

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

library ArrayUtils {
    // internal functions can be used in internal library functions because
    // they will be part of the same code context
    function map(uint[] memory self, function (uint) pure returns (uint) f)
        internal
        pure
        returns (uint[] memory r)
    {
        r = new uint[](self.length);
        for (uint i = 0; i < self.length; i++) {
            r[i] = f(self[i]);
        }
    }

    function reduce(
        uint[] memory self,
        function (uint, uint) pure returns (uint) f
    )
        internal
        pure
        returns (uint r)
    {
        r = self[0];
        for (uint i = 1; i < self.length; i++) {
            r = f(r, self[i]);
        }
    }

    function range(uint length) internal pure returns (uint[] memory r) {
        r = new uint[](length);
        for (uint i = 0; i < r.length; i++) {
            r[i] = i;
        }
    }
}

contract Pyramid {
    using ArrayUtils for *;

    function pyramid(uint l) public pure returns (uint) {
        return ArrayUtils.range(l).map(square).reduce(sum);
    }

    function square(uint x) internal pure returns (uint) {
        return x * x;
    }

    function sum(uint x, uint y) internal pure returns (uint) {
        return x + y;
    }
}

Smart contract – Internal Function Type – Code Breakdown and Analysis

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

Defines the library containing our generic functions map(...) and reduce(...), which receive the function type object defining their actual behavior.

Functions map() and reduce() are well-known in the programming world, and especially the parallel programming world; map() is used to apply the same operation to a range of elements, and reduce() is used to collect or aggregate the results after map() to a single result.

library ArrayUtils {
    // internal functions can be used in internal library functions because
    // they will be part of the same code context

The map() function uses a special feature of library functions: when the function can be seen as a method of an object, it’s idiomatic to call the first parameter self

Function map() will accept function type f as the element that determines the map() behavior.

    function map(uint[] memory self, function (uint) pure returns (uint) f)
        internal
        pure
        returns (uint[] memory r)
    {

Defines r as the resulting array of type uint, iterates over the original self array, and applies the function f to each element.

        r = new uint[](self.length);
        for (uint i = 0; i < self.length; i++) {
            r[i] = f(self[i]);
        }
    }

Iterates over the resulting array contained in self, successively applies the f function (type) to each element of the array, and aggregates the result to the r variable.

We should note that the r variable contains a (starting) value from each previous iteration and carries it to the next iteration to be used in the f function.

    function reduce(
        uint[] memory self,
        function (uint, uint) pure returns (uint) f
    )
        internal
        pure
        returns (uint r)
    {
        r = self[0];
        for (uint i = 1; i < self.length; i++) {
            r = f(r, self[i]);
        }
    }

The range() function is a simple function that initializes the array elements to their index values.

    function range(uint length) internal pure returns (uint[] memory r) {
        r = new uint[](length);
        for (uint i = 0; i < r.length; i++) {
            r[i] = i;
        }
    }
}

Creates a new contract for calculating the sum of squares for a range of numbers l, along with the supporting, behavior-defining functions square() and sum().

contract Pyramid {
    using ArrayUtils for *;

Uses the ArrayUtils library, produces an array of (index-initialized) values from the ArrayUtils.range(l) function, passes it to the map() function, and applies the square() function, yielding a new array of squares.

Then it passes the array of squares to the reduce() function and applies the sum function.

    function pyramid(uint l) public pure returns (uint) {
        return ArrayUtils.range(l).map(square).reduce(sum);
    }

    function square(uint x) internal pure returns (uint) {
        return x * x;
    }

    function sum(uint x, uint y) internal pure returns (uint) {
        return x + y;
    }
}

We might ask ourselves, why didn’t we just use the squares() and sum() functions?

The answer lies in the design decision to have a generic, scalable arrangement of our code, where we can just plug in, add, and maintain a list of functions and apply them to our input data.

We can think of it in a more practical way: when we want a smartphone (a generic function) to have new functionality, we’d add it by installing a specific application; we most certainly wouldn’t want to buy a new phone every time we want to do something new with a phone.

Running the Example: Internal Function Type

This section contains additional information for running the contract. We should expect that our example accounts may change with each refresh/reload of Remix.

To run the example, we will just deploy the Pyramid contract.

Contract Test Scenario: Internal Function Type

  1. Call the pyramid(5) function.
  2. Get the result of 02 + 12 + 22 + 32 + 42 = 0 + 1 + 4 + 9 + 16 = 30.

Smart Contract – External Function Type

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;

contract Oracle {
    struct Request {
        bytes data;
        function(uint) external callback;
    }

    Request[] private requests;
    event NewRequest(uint);

    function query(bytes memory data, function(uint) external callback) public {
        requests.push(Request(data, callback));
        emit NewRequest(requests.length - 1);
    }

    function reply(uint requestID, uint response) public {
        // Here goes the check that the reply comes from a trusted source
        requests[requestID].callback(response);
    }
}


contract OracleUser {
    Oracle constant private ORACLE_CONST = Oracle(address(0x00000000219ab540356cBB839Cbe05303d7705Fa)); // known contract
    uint public exchangeRate;

    function buySomething() public {
        ORACLE_CONST.query("USD", this.oracleResponse);
    }

    function oracleResponse(uint response) public {
        require(
            msg.sender == address(ORACLE_CONST),
            "Only oracle can call this."
        );
        exchangeRate = response;
    }
}

Smart contract – External Function Type – Code Breakdown and Analysis

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;

πŸ’‘ Info: “Blockchain oracles are entities that connect blockchains to external systems, thereby enabling smart contracts to execute based upon inputs and outputs from the real world.”

(https://chain.link/education/blockchain-oracles).

Creates an Oracle smart contract that ties together the associated external function type with the exchange symbol, and uses it in the later callback.

contract Oracle {

Defines an elementary request as a user-defined type Request.

    struct Request {
        bytes data;

The function type is very simple. It is named callback and stored with the variable data (exchange symbol).

        function(uint) external callback;
    }

An array of requests is declared and an event is defined.

    Request[] public requests;
    event NewRequest(uint);

The function query stores the exchange symbol (Request.data) with the callback function type (Request.callback; check the previous article on details regarding the remote function call).

Each request is pushed to the array (indices start with 0 and increment by 1 with each push operation).

    function query(bytes memory data, function(uint) external callback) public {
        requests.push(Request(data, callback));

Emits the signal showing the index location of a request.

        emit NewRequest(requests.length - 1);
    }

Calls the remote (OracleUser) function and sets the remote contract’s exchange rate. The requestID was in an emitted signal. We’ll use it later in our test scenario.

    function reply(uint requestID, uint response) public {
        // Here goes the check that the reply comes from a trusted source
        requests[requestID].callback(response);
    }
}

Creates the OracleUser contract.

contract OracleUser {

Declares the Oracle contract address; we have to update it (underlined) in the OracleUser source code before deploying OracleUser contract.

    Oracle constant private ORACLE_CONST = Oracle(address(0x00000000219ab540356cBB839Cbe05303d7705Fa)); // known contract

Here we’ll use a slight modification of the official example and declare the exchangeRate variable as public to show that the exchange rate is appropriately set.

    uint public exchangeRate;

Calls a remote contract’s, i.e. Oracle’s query() function with an exchange symbol and a callback function as arguments.

    function buySomething() public {
        ORACLE_CONST.query("USD", this.oracleResponse);
    }

Sets the exchange rate, but can only be called by the Oracle contract; this constraint is set by the require clause.

    function oracleResponse(uint response) public {
        require(
            msg.sender == address(ORACLE_CONST),
            "Only oracle can call this."
        );
        exchangeRate = response;
    }
}

Running the Example: External Function Type

This section contains additional information for running the contract. We should expect that our example accounts may change with each refresh/reload of Remix.

To run the example, we will first deploy the Oracle contract, copy its address to the ORACLE_CONST initialization line, and then deploy the OracleUser contract.

Contract Test Scenario: External Function Type

  1. OracleUser contract: call the buySomething() function.
  2. Oracle contract: call the reply(0, 17) function.
  3. OracleUser contract: call the exchangeRate() getter function and check the exchange rate set by the Oracle contract.

Conclusion

Through this article, we analyzed several examples of smart contracts, got more familiar with the principles they use, and improved our understanding in terms of their usefulness and areas of application.

  1. First, we had a clean listing of the internal function type example.
  2. Second, we broke down the code of the internal function type example and analyzed it thoroughly.
  3. Third, we got familiar with a very simple and short topic of how to run the internal function type example.
  4. Fourth, we went through the actual steps of running the example of the internal function type and interpreting the results.
  5. Fifth, we had a clean listing of the external function type example.
  6. Sixth, we broke down the code of the external function type example and analyzed it thoroughly.
  7. Seventh, we got familiar with a very simple and short topic of how to run the external function type example.
  8. Eighth, we went through the actual steps of running the example of the external function type and interpreting the results.

What’s Next?

This tutorial is part of our extended Solidity documentation with videos and more accessible examples and explanations. You can navigate the series here (all links open in a new tab):