Learning Cairo - Variables and Mutability (2)

Learning Cairo - Variables and Mutability (2)

Prerequisite

Install cairo-run.

Variables

Today we are going to learn about variables in Cairo. Let’s start with a simple example:

// filename: print.cairo

use debug::PrintTrait;
fn main() {
    let x = 12;
    x.print();
    x = 20;
    x.print();
}

The code above is straightforward. First, we define a variable x and assign it a value of 12, then we print x. Next, we assign it a value of 20 and print it again. It seems fine, so let’s run cairo-run print.cairo:

error: Cannot assign to an immutable variable.
 --> print.cairo:5:5
    x = 20;
    ^***^

Error: failed to compile: print.cairo

The error message tells us that we cannot assign a value to an immutable variable, which might be a bit difficult to understand. Why are variables immutable? This is related to Cairo’s underlying design. Cairo uses an immutable memory model, which means that once a memory location is written, it cannot be changed, only read. In terms of syntax, variables in Cairo are immutable by default.

So, is there a way to define a true variable? Yes, we can use the mut keyword:

use debug::PrintTrait;
fn main() {
    let mut x = 5;
    x.print();
    x = 6;
    x.print();
}

Here, we add the mut keyword when declaring the variable to indicate that it is mutable. Now, the code can compile and run successfully:

[DEBUG]                                 (raw: 5)

[DEBUG]                                 (raw: 6)

Run completed successfully, returning []

So, what happens at the underlying level when we add the mut keyword? As mentioned earlier, Cairo employs an immutable memory model, where a memory location, once assigned, cannot be changed. However, the variable can be reassigned to a different memory address. This is actually a syntax sugar, where Cairo’s underlying implementation transforms variable mutation operations into variable hiding or shadowing. We’ll discuss this further below.

One thing to note is that we didn’t explicitly specify the variable’s type in the code. This is because Cairo automatically infers the type based on the assigned value. Cairo is a statically-typed language, which means that a variable’s type is determined at the time of its definition. We’ll cover data types in later articles.

Constants

Constants, as the name implies, are values that cannot be changed once defined. They are similar to variables but with some differences.

Firstly, constants are truly immutable and cannot be made mutable using the mut keyword. We can use the const keyword to define a constant, and we must specify its corresponding type at the time of definition, for example:

const IM_CONST: u8 = 1;

Secondly, constants can only be defined in the global scope so that other code can also use them.

Another point is that constant assignments must be explicitly provided at the time of definition and cannot use runtime values. For example, in Solidity, there are two types of constants: constant and immutable. Although both types are assigned values that cannot be changed, the difference is that constant can only use compile-time known values, while immutable can be assigned values known at runtime, such as block.number.

In Cairo, constants are similar to constant constants in Solidity, and they must use compile-time known values.

The naming convention

for constants in Cairo is to use uppercase letters with underscores between words.

Variable Shadowing

Variable shadowing refers to declaring a variable with the same name as an existing variable, effectively hiding the previous variable. This means that the second variable becomes the one in effect.

Let’s look at a simple example:

fn main() {
    let x = 5_u8;
    let x = 12_u128;
    let x = 'hi';
}

In the code above, we declare the variable x three times, each time with a different type. In some other languages, this would result in a compilation error, but in Cairo/Rust, it can be successfully compiled. In this case, the second declaration of x hides the first x, and the third declaration of x hides the second x. In the end, only the third x is effective:

[DEBUG] hi                              (raw: 26729)

Run completed successfully, returning []

Here’s another example:

use debug::PrintTrait;
fn main() {
    let x = 5;
    let x = x + 1;
    {
        let x = x * 2;
        'Inner scope x value is:'.print();
        x.print()
    }
    'Outer scope x value is:'.print();
    x.print();
}

First, we declare x as 5, then we declare x = 5 + 1 = 6. Inside the inner code block, we declare x = 6 * 2 = 12. Let’s see the print results:

[DEBUG] Inner scope x value is:         (raw: 7033328135641142205392067879065573688897582790068499258)

[DEBUG]
                                        (raw: 12)

[DEBUG] Outer scope x value is:         (raw: 7610641743409771490723378239576163509623951327599620922)

[DEBUG]                                 (raw: 6)

Run completed successfully, returning []

The x within the code block is 12, while the outer x is 6. This demonstrates that variable hiding is limited to the corresponding scope. Once we leave the scope, the hiding effect is lost.

Let’s recall the mut keyword we learned earlier. In fact, it is equivalent to variable shadowing at the underlying level. mut is a syntax sugar, and its difference from variable shadowing is that it cannot change the data type, only modify it to the same type. On the other hand, variable shadowing allows modifying the data type.

use debug::PrintTrait;
use traits::Into;
fn main() {
    let x = 2_u64;
    x.print();
    let x = x.into(); // converts x to a felt.
    x.print()
}

In the example above, the first x is of type u64, and the second x is a newly declared variable of type felt252, which hides the first x. This is allowed. However, if we use mut, it becomes:

use debug::PrintTrait;
use traits::Into;
fn main() {
    let mut x = 2_u64;
    x.print();
    x = x.into();
    x.print()
}

Initially, x is of type u64, but when it is reassigned as felt252, it results in an error:

error: Unexpected argument type. Expected: "core::integer::u64", found: "core::felt252".
 --> print.cairo

:6:9
    x = x.into();
        ^******^

Error: failed to compile: print.cairo

Summary

We have learned about variables and constants in Cairo, as well as an important concept called variable shadowing. Variable shadowing can be challenging for beginners to grasp, but with practice, it can be gradually understood and mastered.

You can star our repository, we will closely update it alongside StarkNet rolls out 1.0 fully, or join our Discord if you have any questions or want to contribute.

Call out StarkNet Beta Testers!

Reddio is building developer tools for StarkNet to help you accelerate the process to develop StarkNet applications. We are inviting all of StarkNet developers to join our beta testing group, try out brand-new features and tell us what you think.

https://share.hsforms.com/1E88oQkqMSJifUV1CqR_WrQd30xn

Low latency and free Starknet node awaits!

For a limited time, Reddio is offering unrestricted access to its high-speed StarkNet Node, completely free of charge. This is an unparalleled opportunity to experience the fastest connection with the lowest delay. All you need to do is register an account on Reddio at https://dashboard.reddio.com/ and start exploring the limitless possibilities.

You can discover why Reddio claims the fastest connection by reading more here.