Skip to Main Content

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

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.

Text lined up in three columns with second column having 3 more rows than first and third
This is what it looks like in 235

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.