Some Solidity concepts
Let's stop and cover a few Solidity concepts while we're here: visibility, value types, and reference types. For reference, here's the function we just defined:
contract FizzBuzz {
function fizzbuzz(uint n) public returns (string memory) {
return "fizz";
}
}
Visibility
We've defined fizzbuzz
as a public
function, which means it can be called both internally by other methods in our contract and externally through message sends. There are a few other function visibility modifiers in Solidity: external
functions can be called by other contracts but not internally, internal
functions can only be accessed internally, and private
functions can only be accessed internally and are not visible to derived contracts. Variables have public
, internal
, and private
visibility, too.
Value types
Our fizzbuzz
function takes one parameter n
as an argument and returns a string. The input parameter n
is a uint
, or unsigned 256-bit integer. "Unsigned" means this integer type represents a non-negative integer value. Integers in Solidity are value types, i.e. always copied and passed by value when they are used in arguments and assignments.
256-bit integers are very large: a uint256
can store a value as large as \( 2^{256}-1 \), which is way bigger than the 32-bit and 64-bit integers used by default in most other common programming languages.
To compare the maximum value of the two types, let's jump into a Python shell and look at the difference:
$ python
>>> (2 ** 64) - 1
18446744073709551615
>>> (2 ** 256) - 1
115792089237316195423570985008687907853269984665640564039457584007913129639935
uint vs uint256
The
uint
type is an implicit alias foruint256
, but it's considered good Solidity style to always be explicit and prefer usinguint256
touint
. Integers of different sizes can be defined in steps of 8, e.g.uint8
,uint128
, anduint216
.
Reference types
The return value of our function is (string memory)
, a reference type. Structs, arrays, and mappings are all reference types in Solidity. (Strings are secretly arrays of bytes under the hood, so they are reference types too). Unlike value types, which are copied each time they are used, reference types are passed by reference, so we have to be more careful about how they are used and modified to avoid unexpected mutations.
When we declare a reference type, we must always also declare the "data area" where it will be stored. There are three options: calldata
, memory
, and storage
. In the case of our return value, we're using memory
.
calldata
is a special, immutable, super-temporary location for function arguments. When you can get away with using it, calldata
is a great location because it's immutable, avoids copies, and is cheap to use.
memory
is a temporary location analogous to runtime memory. Every function call gets access to a freshly cleared chunk of memory that can expand as necessary. Writing to memory is much cheaper than writing to storage
, but it still costs gas to read and write.
storage
is a permanent location that is persistent between function calls. It is expensive to read and very expensive to initialize and write. (This is for good reason: any data we write to storage will be replicated on every node in the Ethereum network and stored forever!)
Onward. Let's run our tests again now that we understand our own code.