Control Flow

La capacidad de ejecutar cierto código dependiendo de si una condición es verdadera y de ejecutar código repetidamente mientras una condición es verdadera son bloques de construcción básicos en la mayoría de los lenguajes de programación. Las construcciones más comunes que le permiten controlar el flujo de ejecución del código en Cairo son las expresiones if y los bucles.

if Expressions

Una expresión if le permite ramificar su código según condiciones. Proporciona una condición y luego establece: "Si se cumple esta condición, ejecute este bloque de código. Si no se cumple la condición, no ejecute este bloque de código".

Filename: src/lib.cairo

use debug::PrintTrait;

fn main() {
    let number = 3;

    if number == 5 {
        'condition was true'.print();
    } else {
        'condition was false'.print();
    }
}

Todos las expresiones if comienzan con la palabra clave if, seguido de una condición. En este caso, la condición verifica si la variable number tiene un valor igual a 5. Colocamos el bloque de código a ejecutar si la condición es true inmediatamente después de la condición dentro de llaves.

Opcionalmente, también podemos incluir una expresión else, que elegimos hacer aquí, para dar al programa un bloque de código alternativo para ejecutar si la condición se evalúa como false. Si no proporciona una expresión else y la condición es false, el programa simplemente omitirá el bloque if y pasará al siguiente fragmento de código.

Intente ejecutar este código; debería ver la siguiente salida:

$ cairo-run main.cairo
[DEBUG]	condition was false

Intentaré cambiar el valor de number por uno que haga que la condición sea verdadera para ver qué sucede:

    let number = 5;
$ cairo-run main.cairo
condition was true

También vale la pena señalar que la condición en este código debe ser un bool. Si la condición no es un bool, obtendremos un error.

$ cairo-run main.cairo
thread 'main' panicked at 'Failed to specialize: `enum_match<felt252>`. Error: Could not specialize libfunc `enum_match` with generic_args: [Type(ConcreteTypeId { id: 1, debug_name: None })]. Error: Provided generic argument is unsupported.', crates/cairo-lang-sierra-generator/src/utils.rs:256:9

Handling Multiple Conditions with else if

Puede usar múltiples condiciones combinando if y else en una expresión else if. Por ejemplo:

Filename: src/lib.cairo

use debug::PrintTrait;

fn main() {
    let number = 3;

    if number == 12 {
        'number is 12'.print();
    } else if number == 3 {
        'number is 3'.print();
    } else if number - 2 == 1 {
        'number minus 2 is 1'.print();
    } else {
        'number not found'.print();
    }
}

Este programa tiene cuatro posibles caminos que puede seguir. Después de ejecutarlo, debería ver la siguiente salida:

[DEBUG]	number is 3

When this program executes, it checks each if expression in turn and executes the first body for which the condition evaluates to true. Note that even though number - 2 == 1 is true, we don’t see the output number minus 2 is 1'.print(), nor do we see the number not found text from the else block. That’s because Cairo only executes the block for the first true condition, and once it finds one, it doesn’t even check the rest. Using too many else if expressions can clutter your code, so if you have more than one, you might want to refactor your code. Chapter 6 describes a powerful Cairo branching construct called match for these cases.

Using if in a let statement

Dado que if es una expresión, podemos usarla en el lado derecho de una declaración let para asignar el resultado a una variable.

Filename: src/lib.cairo

use debug::PrintTrait;

fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };

    if number == 5 {
        'condition was true'.print();
    }
}
$ cairo-run main.cairo
[DEBUG]	condition was true

La variable number quedará ligada a un valor basado en el resultado de la expresión if. En este caso, será 5.

Repetition with Loops

A menudo es útil ejecutar un bloque de código más de una vez. Para esta tarea, Cairo proporciona una simple sintaxis de bucle, que recorrerá el código dentro del cuerpo del bucle hasta el final y luego comenzará inmediatamente de vuelta al principio. Para experimentar con bucles, creemos un nuevo proyecto llamado bucles.

Cairo sólo tiene un tipo de bucle por ahora: loop.

Repeating Code with loop

The loop keyword tells Cairo to execute a block of code over and over again forever or until you explicitly tell it to stop.

As an example, change the src/lib.cairo file in your loops directory to look like this:

Filename: src/lib.cairo

use debug::PrintTrait;
fn main() {
    let mut i: usize = 0;
    loop {
        if i > 10 {
            break;
        }
        'again!'.print();
    }
}

When we run this program, we’ll see again! printed over and over continuously until we stop the program manually, because the stop condition is never reached. While the compiler prevents us from writing programs without a stop condition (break statement), the stop condition might never be reached, resulting in an infinite loop. Most terminals support the keyboard shortcut ctrl-c to interrupt a program that is stuck in a continual loop. Give it a try:

$ scarb cairo-run --available-gas=20000000
[DEBUG]	again                          	(raw: 418346264942)

[DEBUG]	again                          	(raw: 418346264942)

[DEBUG]	again                          	(raw: 418346264942)

[DEBUG]	again                          	(raw: 418346264942)

Run panicked with err values: [375233589013918064796019]
Remaining gas: 1050

Note: Cairo prevents us from running program with infinite loops by including a gas meter. The gas meter is a mechanism that limits the amount of computation that can be done in a program. By setting a value to the --available-gas flag, we can set the maximum amount of gas available to the program. Gas is a unit of measurement that expresses the computation cost of an instruction. When the gas meter runs out, the program will stop. In this case, the program panicked because it ran out of gas, as the stop condition was never reached. It is particularly important in the context of smart contracts deployed on Starknet, as it prevents from running infinite loops on the network. If you're writing a program that needs to run a loop, you will need to execute it with the --available-gas flag set to a value that is large enough to run the program.

To break out of a loop, you can place the break statement within the loop to tell the program when to stop executing the loop. Let's fix the infinite loop by adding a making the stop condition i > 10 reachable.

use debug::PrintTrait;
fn main() {
    let mut i: usize = 0;
    loop {
        if i > 10 {
            break;
        }
        'again'.print();
        i += 1;
    }
}

La palabra clave continue le indica al programa que pase a la siguiente iteración del bucle y omita el resto del código en esta iteración. Agreguemos una instrucción continue a nuestro bucle para saltar la declaración print cuando i sea igual a 5.

use debug::PrintTrait;
fn main() {
    let mut i: usize = 0;
    loop {
        if i > 10 {
            break;
        }
        if i == 5 {
            i += 1;
            continue;
        }
        i.print();
        i += 1;
    }
}

Al ejecutar este programa, no se imprimirá el valor de i cuando sea igual a 5.

Returning Values from Loops

One of the uses of a loop is to retry an operation you know might fail, such as checking whether an operation has succeeded. You might also need to pass the result of that operation out of the loop to the rest of your code. To do this, you can add the value you want returned after the break expression you use to stop the loop; that value will be returned out of the loop so you can use it, as shown here:

use debug::PrintTrait;
fn main() {
    let mut counter = 0;

    let result = loop {
        if counter == 10 {
            break counter * 2;
        }
        counter += 1;
    };

    'The result is '.print();
    result.print();
}

Before the loop, we declare a variable named counter and initialize it to 0. Then we declare a variable named result to hold the value returned from the loop. On every iteration of the loop, we check whether the counter is equal to 10, and then add 1 to the counter variable. When the condition is met, we use the break keyword with the value counter * 2. After the loop, we use a semicolon to end the statement that assigns the value to result. Finally, we print the value in result, which in this case is 20.

Summary

You made it! This was a sizable chapter: you learned about variables, data types, functions, comments, if expressions and loops! To practice with the concepts discussed in this chapter, try building programs to do the following:

  • Generate the n-th Fibonacci number.
  • Compute the factorial of a number n.

Now, we’ll review the common collection types in Cairo in the next chapter.

Last change: 2023-11-19, commit: a15432b