How to Extend Builtin Types in Solidity

In Solidity, there is a way to extend builtin types with using A for B directive.

Please note that the extention is active only within the current scope, meaning there is not risk to pollute other scopes by defining native builtin types. This design allows developers to avoid writing bugs that conflicts with other behaviours among libraries.

Example

In this example, let's extend uint[] types using Libraries. Here is a user of that library, that includes uint[] public data that needs to be extended.

// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0 <0.9.0;

contract SearchArray {
    uint[] public data;

    constructor() {
        data.push(0);
        data.push(1);
        data.push(2);
        data.push(4);
        data.push(32);
        data.push(16);
        data.push(8);
    }

    function getData() public view returns (uint[] memory) {
        return data;
    }

    function append(uint value) public {
        data.push(value);
    }

    function has(uint x) public view returns (bool) {
        return data.includes(x);
    }
}

In this SearchArray contract, there is a public data uint array. It is initialized with the following values:

[0, 1, 2, 4, 32, 16, 8]

Find MAX

Let's extend uint[] to have max() function that retuns the maximum number in the array.

assert(32 == data.max());

A new library, Search, is created as follows. Please note that the function accepts uint[] storage self as a first argument. It iterates through its own data, and return the max number in the array.

library Search {
    function max(uint[] storage self)
        public
        view
        returns (uint)
    {
        uint _max = type(uint).min;
        for (uint i = 0; i < self.length; i++) {
            if (self[i] > _max) {
                _max = self[i];
            }
        }
        return _max;
    }
}

Now, how can we extend the native builin types? After importing the library, add using A for B directive as follows.

using Search for uint[];

contract SearchArray {
    //...

    function getMax() public view returns (uint) {
        return data.max();
    }

    //...
}

Now SearchArray can get the maximum value from the data array by calling data.max().

$ truffle console
truffle(development)> let i = await SearchArray.deployed()
undefined
truffle(development)> i.getMax()
BN { negative: 0, words: [ 32, <1 empty item> ], length: 1, red: null }

Find MIN

In a similar fasion, you can define Search.min() function as well.

assert(0 == data.min());

Here is the implementation:

library Search {
    //...

    function min(uint[] storage self)
        public
        view
        returns (uint)
    {
        uint _min = type(uint).max;
        for (uint i = 0; i < self.length; i++) {
            if (self[i] < _min) {
                _min = self[i];
            }
        }
        return _min;
    }

    //...
}

contract SearchArray {
    //...

    function getMin() public view returns (uint) {
        return data.min();
    }

    //...
}

Now data.min() returns 0 as an outcome:

$ truffle console
truffle(development)> let i = await SearchArray.deployed()
undefined
truffle(development)> i.getMin()
BN { negative: 0, words: [ 0, <1 empty item> ], length: 1, red: null }

Included?

Now let's add an another example. Add Search.includes() that returns true when a passed uint value is in the array, otherwise false.

assert(true == data.includes(32));
assert(false == data.includes(64));

Here is the implementation:

library Search {
    //...

    function includes(uint[] storage self, uint value)
        public
        view
        returns (bool)
    {
        for (uint i = 0; i < self.length; i++) {
            if (self[i] == value) {
                return true;
            }
        }
        return false;
    }

    //...
}

contract SearchArray {
    //...

    function has(uint x) public view returns (bool) {
        return data.includes(x);
    }

    //...
}

Now you can call SearchArray.has() to check if a specific value is included in the array or not.

$ truffle console
truffle(development)>  let i = await SearchArray.deployed()
undefined
truffle(development)> i.has(32)
true
truffle(development)> i.has(64)
false

Summary

using A for B is a powerful tool to extend native types only in the current scope. There is a potential to make your source code clearner by using this pattern.

2022-10-31