The Match Control Flow Construct

Cairo tiene una construcción de control de flujo extremadamente poderosa llamada match que te permite comparar un valor con una serie de patrones y luego ejecutar código basado en el patrón que coincide. Los patrones pueden estar compuestos por valores literales, nombres de variables, comodines y muchas otras cosas. El poder de match proviene de la expresividad de los patrones y del hecho de que el compilador confirma que se manejan todos los casos posibles.

Piensa en una expresión match como una máquina clasificadora de monedas: las monedas se deslizan por una pista con agujeros de diferentes tamaños a lo largo de ella, y cada moneda cae por el primer agujero que encuentra en el que encaja. De la misma manera, los valores pasan por cada patrón en un match, y en el primer patrón en el que el valor "encaja", el valor cae en el bloque de código asociado para ser utilizado durante la ejecución.

Speaking of coins, let’s use them as an example using match! We can write a function that takes an unknown US coin and, in a similar way as the counting machine, determines which coin it is and returns its value in cents, as shown in Listing 6-3.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> felt252 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Listing 6-3: An enum and a match expression that has the variants of the enum as its patterns

Desglosemos el match en la función value_in_cents. Primero enumeramos la palabra clave match seguida de una expresión, que en este caso es el valor coin. Esto parece muy similar a una expresión condicional utilizada con if, pero hay una gran diferencia: con if, la condición debe evaluarse a un valor booleano, pero aquí puede ser de cualquier tipo. El tipo de moneda en este ejemplo es el enum Coin que definimos en la primera línea.

A continuación, están los brazos del match. Un brazo tiene dos partes: un patrón y algún código. El primer brazo aquí tiene un patrón que es el valor Coin::Penny(_) y luego el operador => que separa el patrón y el código a ejecutar. El código en este caso es simplemente el valor 1. Cada brazo está separado del siguiente con una coma.

Cuando se ejecuta la expresión match, compara el valor resultante con el patrón de cada brazo, en orden. Si un patrón coincide con el valor, se ejecuta el código asociado con ese patrón. Si ese patrón no coincide con el valor, la ejecución continúa con el siguiente brazo, como en una máquina clasificadora de monedas. Podemos tener tantos brazos como necesitemos: en el ejemplo anterior, nuestro match tiene cuatro brazos.

En Cairo, el orden de los brazos debe seguir el mismo orden que el enum.

El código asociado con cada brazo es una expresión, y el valor resultante de la expresión en el brazo coincidente es el valor que se devuelve para toda la expresión match.

We don’t typically use curly brackets if the match arm code is short, as it is in our example where each arm just returns a value. If you want to run multiple lines of code in a match arm, you must use curly brackets, with a comma following the arm. For example, the following code prints “Lucky penny!” every time the method is called with a Coin::Penny, but still returns the last value of the block, 1:

fn value_in_cents(coin: Coin) -> felt252 {
    match coin {
        Coin::Penny => {
            ('Lucky penny!').print();
            1
        },
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Patterns That Bind to Values

Otra característica útil de los brazos de coincidencia es que pueden vincularse con las partes de los valores que coinciden con el patrón. Así es como podemos extraer valores de las variantes de una enum.

As an example, let’s change one of our enum variants to hold data inside it. From 1999 through 2008, the United States minted quarters with different designs for each of the 50 states on one side. No other coins got state designs, so only quarters have this extra value. We can add this information to our enum by changing the Quarter variant to include a UsState value stored inside it, which we’ve done in Listing 6-4.

#[derive(Drop)]
enum UsState {
    Alabama,
    Alaska,
}

#[derive(Drop)]
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter: UsState,
}

Listing 6-4: A Coin enum in which the Quarter variant also holds a UsState value

Imaginemos que un amigo está tratando de recolectar todas las 50 monedas de cuarto de estado. Mientras clasificamos nuestro cambio suelto por tipo de moneda, también llamaremos el nombre del estado asociado con cada cuarto para que si es uno que nuestro amigo no tiene, puedan agregarlo a su colección.

En la expresión match de este código, agregamos una variable llamada state al patrón que coincide con los valores de la variante Coin::Quarter. Cuando se hace una coincidencia de Coin::Quarter, la variable state se vinculará al valor del estado de ese cuarto. Luego podemos usar state en el código para ese brazo, así:

fn value_in_cents(coin: Coin) -> felt252 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            state.print();
            25
        },
    }
}

Para imprimir el valor de una variante de un enum en Cairo, necesitamos agregar una implementación para la función print de debug::PrintTrait:

impl UsStatePrintImpl of PrintTrait<UsState> {
    fn print(self: UsState) {
        match self {
            UsState::Alabama => ('Alabama').print(),
            UsState::Alaska => ('Alaska').print(),
        }
    }
}

If we were to call value_in_cents(Coin::Quarter(UsState::Alaska)), coin would be Coin::Quarter(UsState::Alaska). When we compare that value with each of the match arms, none of them match until we reach Coin::Quarter(state). At that point, the binding for state will be the value UsState::Alaska. We can then use that binding in the PrintTrait, thus getting the inner state value out of the Coin enum variant for Quarter.

Matching with Options

En la sección anterior, queríamos obtener el valor interno T fuera del caso Some al usar Option<T>; ¡también podemos manejar Option<T> usando match, como lo hicimos con el enum Coin! En lugar de comparar monedas, compararemos las variantes de Option<T>, pero la forma en que funciona la expresión match sigue siendo la misma. Puedes usar opciones importando el trait option::OptionTrait.

Digamos que queremos escribir una función que tome una Opción<u8> y, si hay un valor dentro, añada 1 a ese valor. Si no hay ningún valor dentro, la función debería devolver el valor None y no intentar realizar ninguna operación.

This function is very easy to write, thanks to match, and will look like Listing 6-5.

use debug::PrintTrait;

fn plus_one(x: Option<u8>) -> Option<u8> {
    match x {
        Option::Some(val) => Option::Some(val + 1),
        Option::None => Option::None,
    }
}

fn main() {
    let five: Option<u8> = Option::Some(5);
    let six: Option<u8> = plus_one(five);
    six.unwrap().print();
    let none = plus_one(Option::None);
    none.unwrap().print();
}

Listing 6-5: A function that uses a match expression on an Option<u8>

Tenga en cuenta que los brazos (arms) deben respetar el mismo orden que el enum definido en OptionTrait de la librería central de Cairo.

enum Option<T> {
    Some: T,
    None,
}

Examinemos más detalladamente la primera ejecución de plus_one. Cuando llamamos a plus_one(five), la variable x en el cuerpo de plus_one tendrá el valor Some(5). Luego comparamos eso con cada rama del match:

        Option::Some(val) => Option::Some(val + 1),

¿El valor Option::Some(5) coincide con el patrón Option::Some(val)? ¡Sí lo hace! Tenemos la misma variante. El val se enlaza al valor contenido en Option::Some, por lo que val toma el valor 5. Luego se ejecuta el código en el brazo del match, por lo que sumamos 1 al valor de val y creamos un nuevo valor Option::Some con nuestro total 6 en su interior. Debido a que el primer brazo coincide, no se comparan los demás brazos.

Now let’s consider the second call of plus_one in our main function, where x is Option::None. We enter the match and compare to the first arm:

        Option::Some(val) => Option::Some(val + 1),

El valor Option::Some(5_u8) no coincide con el patrón Option::None, así que continuamos con el siguiente brazo:

#![allow(unused)]
fn main() {
        Option::None => Option::None,
}

It matches! There’s no value to add to, so the program stops and returns the Option::None value on the right side of =>.

Combinar match y enumeraciones es útil en muchas situaciones. Verás este patrón mucho en el código de Cairo: match contra una enumeración, enlaza una variable con los datos internos y luego ejecuta código basado en ella. Es un poco complicado al principio, pero una vez que te acostumbras, desearás tenerlo en todos los lenguajes. Es consistentemente favorito de los usuarios.

Matches Are Exhaustive

Hay otro aspecto de los matches que necesitamos discutir: los patrones de los brazos deben cubrir todas las posibilidades. Considera esta versión de nuestra función plus_one, que tiene un error y no se compilará:

fn plus_one(x: Option<u8>) -> Option<u8> {
    match x {
        Option::Some(val) => Option::Some(val + 1),
    }
}
$ scarb cairo-run
    error: Unsupported match. Currently, matches require one arm per variant,
    in the order of variant definition.
    --> test.cairo:34:5
        match x {
        ^*******^
    Error: failed to compile: ./src/test.cairo

Cairo sabe que no cubrimos todos los casos posibles, ¡e incluso sabe qué patrón olvidamos! Los matches en Cairo son exhaustivos: debemos cubrir todas las posibilidades para que el código sea válido. Especialmente en el caso de Option<T>, cuando Cairo nos impide olvidar manejar explícitamente el caso None, nos protege de asumir que tenemos un valor cuando podríamos tener nulo, lo que hace imposible el error de mil millones de dólares discutido anteriormente.

Match 0 and the _ Placeholder

Usando enums, también podemos tomar acciones especiales para algunos valores particulares, pero para todos los demás valores tomar una acción predeterminada. Actualmente solo se admiten 0 y el operador _.

Imaginemos que estamos implementando un juego en el que obtienes un número aleatorio entre 0 y 7. Si tienes 0, ganas. Para todos los demás valores, pierdes. Aquí hay un match que implementa esa lógica, con el número codificado en lugar de ser un valor aleatorio.

fn did_i_win(nb: felt252) {
    match nb {
        0 => ('You won!').print(),
        _ => ('You lost...').print(),
    }
}

Para el primer brazo, el patrón es el valor literal 0. Para el último brazo, que cubre todos los demás valores posibles, el patrón es el carácter _. Este código compila, aunque no hayamos enumerado todos los valores posibles que puede tener felt252, porque el último patrón coincidirá con todos los valores no enumerados específicamente. Este patrón catch-all cumple el requisito de que match debe ser exhaustivo. Tenga en cuenta que tenemos que poner la rama catch-all en último lugar porque los patrones se evalúan en orden. Si pusiéramos el brazo catch-all antes, los otros brazos nunca se ejecutarían, ¡así que Cairo nos avisará si añadimos brazos después de un catch-all!

Last change: 2023-10-18, commit: 36d2b21