Variables y mutabilidad
Cairo usa un modelo de memoria inmutable, lo que significa que una vez que se escribe en una celda de memoria, no se puede sobrescribir, sino solo leer. Para reflejar este modelo de memoria inmutable, las variables en Cairo son inmutables por defecto. Sin embargo, el lenguaje abstrae este modelo y te da la opción de hacer tus variables mutables. Exploremos cómo y por qué Cairo impone la inmutabilidad, y cómo puedes hacer tus variables mutables.
Cuando una variable es inmutable, una vez que un valor está ligado a un nombre, no puedes cambiar ese valor. Para ilustrar esto, genera un nuevo proyecto llamado variables en tu directorio cairo_projects usando scarb new variables
.
A continuación, en tu nuevo directorio variables, abra src/lib.cairo y sustituya su código por el siguiente, que todavía no compilará:
Filename: src/lib.cairo
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
Save and run the program using scarb cairo-run
. You should receive an error message regarding an immutability error, as shown in this output:
$ scarb cairo-run
Compiling no_listing_01_variables_are_immutable v0.1.0 (listings/ch02-common-programming-concepts/no_listing_01_variables_are_immutable/Scarb.toml)
error: Cannot assign to an immutable variable.
--> listings/ch02-common-programming-concepts/no_listing_01_variables_are_immutable/src/lib.cairo:6:5
x = 6;
^***^
error: could not compile `no_listing_01_variables_are_immutable` due to previous error
error: `scarb metadata` exited with error
This example shows how the compiler helps you find errors in your programs. Compiler errors can be frustrating, but they only mean your program isn’t safely doing what you want it to do yet; they do not mean that you’re not a good programmer! Experienced Caironautes still get compiler errors.
Recibiste el mensaje de error Cannot assign to an immutable variable.
porque intentaste asignar un segundo valor a la variable inmutable x
.
Es importante que obtengamos errores en tiempo de compilación cuando intentamos cambiar un valor designado como inmutable porque esta situación específica puede conducir a errores. Si una parte de nuestro código opera bajo la suposición de que un valor nunca cambiará y otra parte de nuestro código cambia ese valor, es posible que la primera parte del código no haga lo que fue diseñada para hacer. La causa de este tipo de error puede ser difícil de rastrear después de los hechos, especialmente cuando la segunda parte del código cambia el valor sólo a veces.
Cairo, a diferencia de la mayoría de los otros lenguajes, tiene memoria inmutable. Esto hace que una clase completa de errores sea imposible, porque los valores nunca cambiarán inesperadamente. Esto facilita el razonamiento sobre el código.
Pero la mutabilidad puede ser muy útil, y puede hacer que el código sea más cómodo de escribir. Aunque las variables son inmutables por defecto, puedes hacerlas mutables añadiendo mut
delante del nombre de la variable. Añadir mut
también transmite intención a los futuros lectores del código indicando que otras partes del código cambiarán el valor de esta variable.
However, you might be wondering at this point what exactly happens when a variable is declared as mut
, as we previously mentioned that Cairo's memory is immutable. The answer is that the value is immutable, but the variable isn't. The value associated to the variable can be changed. Assigning to a mutable variable in Cairo is essentially equivalent to redeclaring it to refer to another value in another memory cell, but the compiler handles that for you, and the keyword mut
makes it explicit. Upon examining the low-level Cairo Assembly code, it becomes clear that variable mutation is implemented as syntactic sugar, which translates mutation operations into a series of steps equivalent to variable shadowing. The only difference is that at the Cairo level, the variable is not redeclared so its type cannot change.
Por ejemplo, cambiemos src/lib.cairo por lo siguiente:
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
Cuando ejecutamos el programa ahora, obtenemos esto:
$ scarb cairo-run
Compiling no_listing_02_adding_mut v0.1.0 (listings/ch02-common-programming-concepts/no_listing_02_adding_mut/Scarb.toml)
Finished `dev` profile target(s) in 4 seconds
Running no_listing_02_adding_mut
The value of x is: 5
The value of x is: 6
Run completed successfully, returning []
Se nos permite cambiar el valor ligado a x
de 5
a 6
cuando se usa mut
. En última instancia, la decisión de utilizar la mutabilidad o no es suya y depende de lo que usted piensa que es más claro en esa situación particular.
Constantes
Al igual que las variables inmutables, las constants son valores que están vinculados a un nombre y no se les permite cambiar, pero hay algunas diferencias entre las constantes y las variables.
First, you aren’t allowed to use mut
with constants. Constants aren’t just immutable by default—they’re always immutable. You declare constants using the const
keyword instead of the let
keyword, and the type of the value must be annotated. We’ll cover types and type annotations in the next section, “Data Types”, so don’t worry about the details right now. Just know that you must always annotate the type.
Constant variables can be declared with any usual data type, including structs, enums and fixed-size arrays.
Las constantes solo se pueden declarar en el ámbito global, lo que las hace útiles para valores que muchas partes del código necesitan conocer.
The last difference is that constants may natively be set only to a constant expression, not the result of a value that could only be computed at runtime.
Here’s an example of constants declaration:
struct AnyStruct {
a: u256,
b: u32,
}
enum AnyEnum {
A: felt252,
B: (usize, u256),
}
const ONE_HOUR_IN_SECONDS: u32 = 3600;
const STRUCT_INSTANCE: AnyStruct = AnyStruct { a: 0, b: 1 };
const ENUM_INSTANCE: AnyEnum = AnyEnum::A('any enum');
const BOOL_FIXED_SIZE_ARRAY: [bool; 2] = [true, false];
Nonetheless, it is possible to use the consteval_int!
macro to create a const
variable that is the result of some computation:
const ONE_HOUR_IN_SECONDS: u32 = consteval_int!(60 * 60);
We will dive into more detail about macros in the dedicated section.
La convención de nomenclatura de Cairo para las constantes es utilizar mayúsculas con guiones bajos entre palabras.
Las constantes son válidas durante todo el tiempo que se ejecuta un programa, dentro del ámbito en el que fueron declaradas. Esta propiedad hace que las constantes sean útiles para los valores en el dominio de su aplicación que varias partes del programa podrían necesitar conocer, como el número máximo de puntos que cualquier jugador de un juego puede ganar o la velocidad de la luz.
Nombrar los valores codificados en el programa como constantes y usarlas en todo el código es útil para transmitir el significado de ese valor a los futuros mantenedores del código. También ayuda a tener solo un lugar en tu código que necesitarías cambiar si el valor codificado necesitara ser actualizado en el futuro.
Shadowing
El shadowing (sombreado) de variables se refiere a la declaración de una nueva variable con el mismo nombre que una variable anterior. Los Caironautas dicen que la primera variable está sombreada por la segunda, lo que significa que la segunda variable es la que el compilador verá cuando uses el nombre de la variable. En efecto, la segunda variable eclipsa a la primera, tomando cualquier uso del nombre de la variable para sí misma hasta que ella misma sea sombreada o termine el ámbito. Podemos sombrear una variable usando el mismo nombre de la variable y repitiendo el uso de la palabra clave let
de la siguiente manera:
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("Inner scope x value is: {}", x);
}
println!("Outer scope x value is: {}", x);
}
Este programa primero asigna un valor de 5
a x
. Luego crea una nueva variable x
repitiendo let x =
, tomando el valor original y sumando 1
, por lo que el valor de x
es ahora 6
. Luego, dentro de un ámbito interno creado con llaves, la tercera instrucción let
también sombrea x
y crea una nueva variable, multiplicando el valor anterior por 2
para darle a x
un valor de 12
. Cuando ese ámbito termina, la sombra interna termina y x
vuelve a ser 6
. Al ejecutar este programa, se mostrará lo siguiente:
$ scarb cairo-run
Compiling no_listing_03_shadowing v0.1.0 (listings/ch02-common-programming-concepts/no_listing_03_shadowing/Scarb.toml)
Finished `dev` profile target(s) in 4 seconds
Running no_listing_03_shadowing
Inner scope x value is: 12
Outer scope x value is: 6
Run completed successfully, returning []
El shadowing es diferente a marcar una variable como mut
, porque obtendremos un error en tiempo de compilación si intentamos reasignar a esta variable sin usar la palabra clave let
. Al usar let
, podemos realizar algunas transformaciones en un valor pero hacer que la variable sea inmutable después de que se hayan completado esas transformaciones.
Otra distinción entre mut
y el shadowing es que al usar nuevamente la palabra clave let
, estamos creando efectivamente una nueva variable, lo que nos permite cambiar el tipo del valor mientras reutilizamos el mismo nombre. Como se mencionó anteriormente, el shadowing de variables y las variables mutables son equivalentes a un nivel más bajo. La única diferencia es que al hacer shadowing de una variable, el compilador no mostrará un error si cambias su tipo. Por ejemplo, supongamos que nuestro programa realiza una conversión de tipo entre los tipos u64
y felt252
.
fn main() {
let x: u64 = 2;
println!("The value of x is {} of type u64", x);
let x: felt252 = x.into(); // converts x to a felt, type annotation is required.
println!("The value of x is {} of type felt252", x);
}
El primer variable x
tiene un tipo u64
, mientras que la segunda variable x
tiene un tipo felt252
. Por lo tanto, el shadowing nos ahorra tener que inventar diferentes nombres, como x_u64
y x_felt252
; en su lugar, podemos reutilizar el nombre más simple x
. Sin embargo, si intentamos usar mut
para esto, como se muestra aquí, obtendremos un error en tiempo de compilación:
fn main() {
let mut x: u64 = 2;
println!("The value of x is: {}", x);
x = 5_u8;
println!("The value of x is: {}", x);
}
El error indica que se esperaba un u64
(el tipo original), pero se obtuvo un tipo diferente:
$ scarb cairo-run
Compiling no_listing_05_mut_cant_change_type v0.1.0 (listings/ch02-common-programming-concepts/no_listing_05_mut_cant_change_type/Scarb.toml)
error: Unexpected argument type. Expected: "core::integer::u64", found: "core::integer::u8".
--> listings/ch02-common-programming-concepts/no_listing_05_mut_cant_change_type/src/lib.cairo:6:9
x = 5_u8;
^**^
error: could not compile `no_listing_05_mut_cant_change_type` due to previous error
error: `scarb metadata` exited with error
Ahora que hemos explorado cómo funcionan las variables, veamos otros tipos de datos que pueden tener.