Learning Rust: Pattern Matching
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 defnitely 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.
Learning Rust series
- Learning Rust #1: Pattern Matching (you are here)
- Learning Rust #2: Option & Result
- Learning Rust #3: crates.io & publishing your package
- Learning Rust #4: Parsing JSON with strong types
- Learning Rust #5: Rustlings
- Learning Rust #6: Understanding ownership in Rust
- Learning Rust #7: Learn from the community
- Learning Rust #8: What's next?
- Learning Rust #9: A talk about rustlings
- Learning Rust #10: Added new feature with a HashMap
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.