Last December I finally started learning Rust and last month I built and published my first app with Rust: 235. Learning Rust is my new monthly blog series that is definitely not a tutorial but rather a place for me to keep track of my learning and write about things I've learned along the way.
My process of learning is reading some guides and docs (with Rust, I started with The Rust Book and Rust by Example), then I try to make something that works, ask a lot of questions from the community, improve, fix bugs, read some more and now I'm writing blog posts which require me to do more research and help me futher improve my program.
The first concept that I've really liked and that I have not used in other languages is the match
operator for pattern matching. In correct use cases, it can make the code nice and easy to read but it also can end up becoming a messy spaghetti. I'm currently still trying to figure out when it's best to use and when not.
Basics of pattern matching
First use case I have for match is to map team name abbreviations from the API into the city names I want to use in the app:
let str_form = match abbr {
"BOS" => "Boston",
"BUF" => "Buffalo",
"NJD" => "New Jersey",
"NYI" => "NY Islanders",
"NYR" => "NY Rangers",
"PHI" => "Philadelphia",
"PIT" => "Pittsburgh",
"WSH" => "Washington",
"CAR" => "Carolina",
"CHI" => "Chicago",
"CBJ" => "Columbus",
"DAL" => "Dallas",
"DET" => "Detroit",
"FLA" => "Florida",
"NSH" => "Nashville",
"TBL" => "Tampa Bay",
"ANA" => "Anaheim",
"ARI" => "Arizona",
"COL" => "Colorado",
"LAK" => "Los Angeles",
"MIN" => "Minnesota",
"SJS" => "San Jose",
"STL" => "St. Louis",
"VGK" => "Vegas",
"CGY" => "Calgary",
"EDM" => "Edmonton",
"MTL" => "Montreal",
"OTT" => "Ottawa",
"TOR" => "Toronto",
"VAN" => "Vancouver",
"WPG" => "Winnipeg",
_ => "[unknown]",
};
(Editor's note: while writing this blog post, I'm exposing myself to how bad my variable naming is in this project. That's partially because so much mental energy is spent on figuring out how to write Rust that there's not much left for thinking about good names. I'll fix 'em little by little as I refactor so it might be that at the moment you're reading this, I have already renamed them in the source.
Another example of how I use match
in 235 is when deciding how to print lines of goals:
let score_iter = home_scores.into_iter().zip_longest(away_scores.into_iter());
for pair in score_iter {
match pair {
Both(l, r) => print_full(l, r),
Left(l) => print_left(l),
Right(r) => print_right(r),
}
}
Here, I have score_iter
(again, not the greatest name, I admit) which is a Zip that supports uneven lengths. For example, it could look something like this (this example is pseudo, the actual data is more complex. _
denotes a missing value):
[
((Appleton, 2), (Boeser, 0)),
(_, (Hoglander, 8)),
(_, (MacEwen, 26)),
(_, (Boeser, 57))
]
By matching these pairs, each line will either be (itertools::EitherOrBoth::
)Both
which means both values are there, Left
meaning only the left value exists and Right
for only right value existing. Matching these with match pair
, it's nice and clean to run the corresponding print function.
This second match
example also showcases binding where we bind the values into variables l
and r
so we can refer to them inside the expressions.
Options and Results
Another very handy use case for match
is with Result
and Option
. My main logic when running 235 on the command line is done within api()
function that returns Result<(), reqwest::Error>
.
match api() {
Ok(_) => (),
Err(err) => println!("{:?}", err),
};
Since Result
will either be Ok
or Err
, I'm matching Ok
with no-op and Err
with printing out the error into the console.
Example with doing similar with Option
is
games.into_iter().for_each(|game| match game {
Some(game) => print_game(&game),
None => (),
})
Here, if a game was parsed correctly, it will be Some
and if it didn't, it will be None
so we can just skip the None
case and only print the games that were parsed correctly from the data.
Guards
One thing I haven't used yet and only learned about it while doing research for this blog post is guards. They are essentially additional if clauses combined with a pattern so you can differentiate similar values based on extra condition.
The example in Rust by Example is this:
let pair = (2, -2);
match pair {
(x, y) if x == y => println!("These are twins"),
(x, y) if x + y == 0 => println!("Antimatter, kaboom!"),
(x, _) if x % 2 == 1 => println!("The first one is odd"),
_ => println!("No correlation..."),
}
I haven't yet run into cases where I'd have use for the guards but wanted to record it here so you'll know it exists.
Conclusion
All in all, match
might be my favorite syntactic thing in Rust so far. Sometimes it feels bit too much like a hammer (in sense of if all you have is a hammer, every problem starts to look like a nail) in that I tend to overuse it even when other means would make more sense. But I'm sure it's something that I'll find the right balance as I code more with Rust.
Top comments (2)
I'm just learning rust now (working my way through The Book), and working on a non-trivial personal project.
I hadn't gotten to the guards section yet. I can foresee some scenarios that this could come in handy, more so than a match-within-a-match construct.
Side note: I'm loving the concepts in rust as a language that protects you from yourself, while maintaining speed. :) - even if it sometimes feels painful to appease the borrow checker
You do not need to
match
all elements in an iterator to skip some. Justfilter()
them out, or in your case usingOption
, justflatten()
! 👍