Eşleştirme Kontrol Akış Yapısı
Cairo has an extremely powerful control flow construct called match
that allows you to compare a value against a series of patterns and then execute code based on which pattern matches. Patterns can be made up of literal values, variable names, wildcards, and many other things. The power of match
comes from the expressiveness of the patterns and the fact that the compiler confirms that all possible cases are handled.
Think of a match
expression as being like a coin-sorting machine: coins slide down a track with variously sized holes along it, and each coin falls through the first hole it encounters that it fits into. In the same way, values go through each pattern in a match, and at the first pattern the value “fits”, the value falls into the associated code block to be used during execution.
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-1.
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-1: An enum and a match
expression that has the variants of the enum as its patterns
Let’s break down the match
expression in the value_in_cents
function. First, we list the match
keyword followed by an expression, which in this case is the value coin
. This seems very similar to a conditional expression used with the if
statement, but there’s a big difference: with if
, the condition needs to evaluate to a boolean value, but here it can be any type. The type of coin
in this example is the Coin
enum that we defined on the first line.
Next are the match
arms. An arm has two parts: a pattern and some code. The first arm here has a pattern that is the value Coin::Penny
and then the =>
operator that separates the pattern and the code to run. The code in this case is just the value 1
. Each arm is separated from the next with a comma.
When the match
expression executes, it compares the resultant value against the pattern of each arm, in the order they are given. If a pattern matches the value, the code associated with that pattern is executed. If that pattern doesn’t match the value, execution continues to the next arm, much as in a coin-sorting machine. We can have as many arms as we need: in the above example, our match
has four arms.
Her kol ile ilişkili kod bir ifadedir ve eşleşen kolun ifadesinin sonuç değeri, tüm match ifadesi için döndürülen değerdir.
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 => {
println!("Lucky penny!");
1
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
Değerlere Bağlanan Desenler
Another useful feature of match
arms is that they can bind to the parts of the values that match the pattern. This is how we can extract values out of enum variants.
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-2.
#[derive(Drop, Debug)] // Debug so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
}
#[derive(Drop)]
enum Coin {
Penny,
Nickel,
Dime,
Quarter: UsState,
}
Listing 6-2: A Coin
enum in which the Quarter
variant also holds a UsState
value
Hayal edelim ki bir arkadaşımız tüm 50 eyalet çeyreğini toplamaya çalışıyor. Bozuk paralarımızı madeni para türüne göre sıralarken, her çeyreğin ilişkili olduğu eyaletin adını da söyleyelim ki, arkadaşımızın koleksiyonunda olmayan bir eyaletse, onu koleksiyonlarına ekleyebilsinler.
In the match
expression for this code, we add a variable called state
to the pattern that matches values of the variant Coin::Quarter
. When a Coin::Quarter
matches, the state
variable will bind to the value of that quarter’s state. Then we can use state
in the code for that arm, like so:
fn value_in_cents(coin: Coin) -> felt252 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
},
}
}
Because state
is an UsState
enum which implements the Debug
trait, we can print state
value with println!
macro.
Note:
{:?}
is a special formatting syntax that allows to print a debug form of the parameter passed to theprintln!
macro. You can find more information about it in Appendix C.
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 println!
macro, thus getting the inner state value out of the Coin
enum variant for Quarter
.
Option<T>
ile Eşleşme
Önceki bölümde, Option<T>
kullanırken Some
durumundaki iç T
değerini çıkarmak istedik; Coin
enumu ile yaptığımız gibi Option<T>
'yi match
kullanarak da ele alabiliriz! Madeni paraları karşılaştırmak yerine Option<T>
'nin varyantlarını karşılaştıracağız, ancak match
ifadesinin çalışma şekli aynı kalır.
Let’s say we want to write a function that takes an Option<u8>
and, if there’s a value inside, adds 1
to that value. If there is no value inside, the function should return the None
value and not attempt to perform any operations.
This function is very easy to write, thanks to match
, and will look like Listing 6-3.
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);
let none = plus_one(Option::None);
}
Listing 6-3: A function that uses a match
expression on an Option<u8>
Let’s examine the first execution of plus_one
in more detail. When we call plus_one(five)
, the variable x
in the body of plus_one
will have the value Some(5)
. We then compare that against each match
arm:
Option::Some(val) => Option::Some(val + 1),
Does Option::Some(5)
value match the pattern Option::Some(val)
? It does! We have the same variant. The val
binds to the value contained in Option::Some
, so val
takes the value 5
. The code in the match
arm is then executed, so we add 1
to the value of val
and create a new Option::Some
value with our total 6
inside. Because the first arm matched, no other arms are compared.
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),
Option::Some(val)
değeri Option::None
deseniyle eşleşmez, bu yüzden bir sonraki kola devam ederiz:
Option::None => Option::None,
It matches! There’s no value to add to, so the matching construct ends and returns the Option::None
value on the right side of =>
.
match
ve enumları birleştirmek birçok durumda yararlıdır. Bu deseni Cairo kodunda çok göreceksiniz: bir enuma karşı match
yapın, veri içindeki bir değişkene bir değişken bağlayın ve sonra ona göre kod çalıştırın. İlk başta biraz zor olabilir, ama alıştığınızda, tüm dillerde bunu isteyeceksiniz. Tutarsız olarak bir kullanıcı favorisidir.
Eşleşmeler Kapsamlıdır
There’s one other aspect of match
we need to discuss: the arms’ patterns must cover all possibilities. Consider this version of our plus_one
function, which has a bug and won’t compile:
fn plus_one(x: Option<u8>) -> Option<u8> {
match x {
Option::Some(val) => Option::Some(val + 1),
}
}
None
durumunu ele almadık, bu yüzden bu kod bir hata oluşturacak. Neyse ki, bu Cairo'nun yakalayabileceği bir hata. Bu kodu derlemeyi denediğimizde, bu hatayı alırız:
$ scarb cairo-run
Compiling no_listing_08_missing_match_arm v0.1.0 (listings/ch06-enums-and-pattern-matching/no_listing_09_missing_match_arm/Scarb.toml)
error: Missing match arm: `None` not covered.
--> listings/ch06-enums-and-pattern-matching/no_listing_09_missing_match_arm/src/lib.cairo:5:5
match x {
^*******^
error: could not compile `no_listing_08_missing_match_arm` due to previous error
error: `scarb metadata` exited with error
Cairo knows that we didn’t cover every possible case, and even knows which pattern we forgot! Matches in Cairo are exhaustive: we must exhaust every last possibility in order for the code to be valid. Especially in the case of Option<T>
, when Cairo prevents us from forgetting to explicitly handle the None
case, it protects us from assuming that we have a value when we might have null, thus making the billion-dollar mistake discussed earlier impossible.
_` Yer Tutucusu ile Her Şeyi Yakalama
Using enums, we can also take special actions for a few particular values, but for all other values take one default action. _
is a special pattern that matches any value and does not bind to that value. You can use it by simply adding a new arm with _
as the pattern for the last arm of the match
expression.
Imagine we have a vending machine that only accepts Dime coins. We want to have a function that processes inserted coins and returns true
only if the coin is accepted.
İşte bu mantığı uygulayan bir vending_machine_accept
fonksiyonu:
fn vending_machine_accept(coin: Coin) -> bool {
match coin {
Coin::Dime => true,
_ => false,
}
}
Bu örnek de tükenmişlik gereksinimini karşılıyor çünkü son kolda açıkça diğer tüm değerleri görmezden geliyoruz; hiçbir şeyi unutmadık.
Cairo'da, desenin değerini kullanmanıza izin veren herhangi bir yakalama-tümü deseni yoktur.
Multiple Patterns with the |
Operator
match
ifadelerinde, |
sözdizimini kullanarak birden fazla deseni eşleştirebilirsiniz, bu da desen veya operatörüdür.
Örneğin, aşağıdaki kodda vending_machine_accept
fonksiyonunu hem Dime
hem de Quarter
bozuk paralarını tek bir kolda kabul edecek şekilde değiştirdik:
fn vending_machine_accept(coin: Coin) -> bool {
match coin {
Coin::Dime | Coin::Quarter => true,
_ => false,
}
}
Matching Tuples
It is possible to match tuples. Let's introduce a new DayType
enum:
#[derive(Drop)]
enum DayType {
Week,
Weekend,
Holiday,
}
Now, let's suppose that our vending machine accepts any coin on weekdays, but only accepts quarters and dimes on weekends and holidays. We can modify the vending_machine_accept
function to accept a tuple of a Coin
and a Weekday
and return true
only if the given coin is accepted on the specified day:
fn vending_machine_accept(c: (DayType, Coin)) -> bool {
match c {
(DayType::Week, _) => true,
(_, Coin::Dime) | (_, Coin::Quarter) => true,
(_, _) => false,
}
}
Writing (_, _)
for the last arm of a tuple matching pattern might feel superfluous. Hence, we can use the _ =>
syntax if we want, for example, that our vending machine only accepts quarters on weekdays:
fn vending_week_machine(c: (DayType, Coin)) -> bool {
match c {
(DayType::Week, Coin::Quarter) => true,
_ => false,
}
}
Matching felt252
and Integer Variables
You can also match felt252
and integer variables. This is useful when you want to match against a range of values. However, there are some restrictions:
- Only integers that fit into a single
felt252
are supported (i.e.u256
is not supported). - İlk kolun 0 olması gerekir.
- Each arm must cover a sequential segment, contiguously with other arms.
0 ile 5 arasında bir sayı almak için altı yüzlü bir zar attığınız bir oyun uyguladığımızı hayal edin. 0, 1 veya 2'niz varsa kazanırsınız. 3'ünüz varsa, tekrar zar atabilirsiniz. Diğer tüm değerler için kaybedersiniz.
Here's a match that implements that logic:
fn roll(value: u8) {
match value {
0 | 1 | 2 => println!("you won!"),
3 => println!("you can roll again!"),
_ => println!("you lost..."),
}
}
Bu kısıtlamaların Cairo'nun gelecek sürümlerinde gevşetilmesi planlanmaktadır.