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(); }
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!