Amir Razak's Blog

Neat pattern matching tricks in Rust

Rust has some neat pattern matching tricks, but even with that, due to the borrow checker and options, some inherently simple tasks that can be one line in python could turn (if done naively) into verbose, multiple lines of bounds checking. This is pretty surface level information, but it might be useful to some who hasn't seen a lot of rust before.

Take in case of a token and a parser for an interpreter, with these definitions

struct Token {
    token_type: TokenType, // an enum
    ...
}

struct Parser {
    tokens: Peekable<Token>, // I'll spare the exact details, it's some Box dyn Iterator thing that's irrelevant now
    ...
}

Where Peekable<Token> contains a method that has peek() -> Option<Token> to check for the next token and next() -> Option<Token> to consume it.

In a case, say, where you just finished parsing an expression, and need to check if the next token is a Semicolon, return a statement containing the expression if there is and return an Error otherwise. Something like below written in python.

if (token := self.tokens.peek()) is not None and token.token_type == TokenType.SEMICOLON:
    self.tokens.next() # consume token and then do things below
else:
    return Error("expected a semicolon")

Very simple, but in rust it's somewhat more verbose, we will slowly iterate through the different ways we can do this. Here is the naive way to do it.

if let Some(tokens) = self.tokens.peek() {
    if tokens.token_type == TokenType::Semicolon {
        self.tokens.next(); // consume token and then do things below
    } else {
        return Err(ParserError::ExpectedSemicolon)
    }
} else {
    return Err(ParserError::ExpectedSemicolon)
}

Yeah... there's probably a better way to do this. Surely match statements would help.

match self.tokens.peek() {
    Some(token) => {
        if token.token_type == TokenType::Semicolon {
            self.tokens.next() // and then do things below
        } else { 
            return Err(ParseError::ExpectedSemicolon) 
        }
    }
    None => return Err(ParserError::ExpectedSemicolon)
}

It's marginally better, if you've gone through the rust book you'd find that we can do match guards, which make it nicer.

match self.tokens.peek() {
    Some(token) if token.token_type == TokenType::Semicolon => {
        self.tokens.next() // and then do things
    }
    _ => return Err(ParserError::ExpectedSemicolon)
}

Great, this looks much nicer. But what if you wanted to match into an Algebraic Data Type (ADT, fancy way of saying enums) such as TokenType::Identifier(String).

match self.tokens.peek() {
    Some(token) if discriminant(token.token_type) == discriminant(TokenType::Identifier("".to_string)) => {
        self.tokens.next() // and then do things
    }
    _ => return Err(ParserError::ExpectedSemicolon)
}

Surely there's something better, you can't if let guard cause its still experimental, and _ only works on left hand side in most cases. Here we can go three ways.

We can use destructuring.

match self.tokens.peek() {
    Some(token @ Token{token_type: TokenType::Identifier(_), ..})  => {
        self.tokens.next() // and then do things
    }
    _ => return Err(ParserError::ExpectedSemicolon)
}

We can use matches!(), which is a very cute macro.

match self.tokens.peek() {
    Some(token) if matches!(token.token_type, TokenType::Identifier(_)) => {
        self.tokens.next() // and then do things
    }
    _ => return Err(ParserError::ExpectedSemicolon)
}

Or we can use filter on Option, which basically filters if it is a Some and disregards it if its a None.

match self.tokens
    .peek()
    .filter(|token| matches!(token.token_type, TokenType::Identifier(_))) {
    Some(token) => {
        self.tokens.next() // and then do things
    }
    _ => return Err(ParserError::ExpectedSemicolon)
}

In the end, which of the three is up to preference. You can also use is_some_and() and probably a few others that I missed, but overall I think these are the nicer ones. Overall, they are fairly nice to work with and has made using rust with it's ADTs very enjoyable.