Introduction
How can we check if a value matches a particular Rust pattern without needing to extract or bind any part of that value? While if let
expressions or standard match
blocks are powerful tools for pattern matching, they typically involve extracting data. Furthermore, we can't directly check against an enum variant's initializer (like UserRole::Admin
) as if it were a type, because it acts more like a function constructing the value. So, how do we perform a simple check elegantly?
Enter the std::matches!
macro. Using this macro, we can perform boolean checks to see if a value conforms to a given pattern. Despite the versatility of the main match
expression, sometimes we simply need a straightforward true/false result, and matches!
provides exactly that.
The Traditional Approach: Verbose Pattern Checks
Traditionally, to check if a value matches a specific pattern and get a boolean result, we often use a full match
statement. This involves writing an arm for the pattern we're interested in to return true
, and a catch-all arm (_)
to return false
.
For a single, simple check, this looks acceptable. However, as the number of checks increases or the patterns become more complex, this approach can lead to verbose and somewhat cluttered code.
Consider the example below, which might mirror logic found in a typical codebase. Notice the repetition and boilerplate required just to get boolean flags:
// Define the different enum types for user attributes (same as before)
#[derive(Debug, PartialEq)]
enum UserRole {
Admin,
Moderator,
Regular,
}
#[derive(Debug, PartialEq)]
enum UserStatus {
Active,
Inactive,
Suspended,
}
#[derive(Debug, PartialEq)]
enum UserTier {
Free,
Basic,
Premium,
}
// Define the User struct with enum fields (same as before)
#[derive(Debug)]
struct User {
username: String,
role: UserRole,
status: UserStatus,
tier: UserTier,
}
// Function to process user based on attribute checks using standard match statements
fn process_user_based_on_attributes_verbose(user: &User) {
println!("Processing user (Verbose Match): {}", user.username);
// Use standard match for boolean checks on individual fields
// This is where the verbosity is introduced compared to matches!
let is_admin = match user.role {
UserRole::Admin => true, // Need an arm for the pattern we care about
_ => false, // And an arm for everything else
};
let is_active = match user.status {
UserStatus::Active => true,
_ => false,
};
let is_premium = match user.tier {
UserTier::Premium => true,
_ => false,
};
// Use standard match to check combinations of fields (matching on a tuple of references)
// Again, notice the structure needed just for a boolean
let is_admin_and_active = match (&user.role, &user.status) {
(UserRole::Admin, UserStatus::Active) => true,
_ => false,
};
let is_regular_and_inactive = match (&user.role, &user.status) {
(UserRole::Regular, UserStatus::Inactive) => true,
_ => false,
};
// Checking multiple patterns using | (OR) within the match arms
let is_premium_or_moderator = match (&user.tier, &user.role) {
(UserTier::Premium, _) | (_, UserRole::Moderator) => true,
_ => false,
};
// Perform actions based on the boolean results (this part remains similar)
if is_admin {
println!(" -> User is an Admin. Granting elevated privileges.");
}
if is_active && is_premium {
println!(" -> User is Active and Premium. Offering exclusive features.");
} else if is_active {
println!(" -> User is Active. Standard access granted.");
} else {
println!(" -> User is Inactive or Suspended. Limiting access.");
}
if is_admin_and_active {
println!(" -> Specific case: Active Admin detected. Special workflow.");
}
if is_regular_and_inactive {
println!(" -> Specific case: Inactive Regular user. Consider outreach.");
}
if is_premium_or_moderator {
println!(" -> Specific case: Premium user or Moderator detected.");
}
println!("---\n"); // Separator for clarity
}
fn main() {
// Create some example users (same as before)
let admin_active_premium = User {
username: "admin_pro".to_string(),
role: UserRole::Admin,
status: UserStatus::Active,
tier: UserTier::Premium,
};
let regular_inactive_free = User {
username: "basic_user".to_string(),
role: UserRole::Regular,
status: UserStatus::Inactive,
tier: UserTier::Free,
};
let moderator_active_basic = User {
username: "mod_user".to_string(),
role: UserRole::Moderator,
status: UserStatus::Active,
tier: UserTier::Basic,
};
let admin_suspended_basic = User {
username: "suspended_admin".to_string(),
role: UserRole::Admin,
status: UserStatus::Suspended,
tier: UserTier::Basic,
};
// Process each user using the verbose function
process_user_based_on_attributes_verbose(&admin_active_premium);
process_user_based_on_attributes_verbose(®ular_inactive_free);
process_user_based_on_attributes_verbose(&moderator_active_basic);
process_user_based_on_attributes_verbose(&admin_suspended_basic);
}
In the code above, we see multiple match
statements just to derive boolean values representing user attributes. While we could potentially embed the logic directly within a single, more complex match
block using Rust's advanced pattern matching, this can quickly escalate complexity, especially when dealing with computed states (states determined by calculations during matching, not just direct variant checks). This verbose approach is functional, but perhaps not ideal for readability or maintainability.
Introducing the std::matches!
Macro
Since Rust 1.42, the std::matches!
macro has been part of the standard library (std)
, meaning we don't need any special imports to use it. It has become the standard tool for these kinds of boolean pattern checks.
Under the hood, matches!
is essentially a wrapper around a match
expression specifically designed for this task:
#[macro_export]
macro_rules! matches {
($expression:expr, $( $pattern:pat )|+ $( if $guard: expr )?) => {
match $expression {
$( $pattern )|+ $( if $guard )? => true,
_ => false
}
}
}
Its purpose is simple: check if an expression matches one or more provided patterns ($pattern)
and return true
if it does, false
otherwise. Additionally, it supports match guards (if $guard)
, allowing an extra condition to be checked only if the pattern itself matches.
Here's the basic syntax in action:
let foo = 'f';
// Check if foo is any uppercase or lowercase letter
assert!(matches!(foo, 'A'..='Z' | 'a'..='z'));
let bar = Some(4);
// Check if bar is Some(x) AND x is greater than 2
assert!(matches!(bar, Some(x) if x > 2));
As seen in the second example, the if
guard provides a way to add conditions: the macro evaluates to true
only if the expression matches the pattern and the if
condition evaluates to true
.
Let's see how this knowledge helps improve our previous user processing example.
Matches! in Action: Concise Pattern Checking
We can refactor the previous example to use matches!
directly within conditional statements like if
, making the code significantly more concise and readable.
// Enums and Struct definitions remain the same as before...
#[derive(Debug, PartialEq)]
enum UserRole { Admin, Moderator, Regular }
#[derive(Debug, PartialEq)]
enum UserStatus { Active, Inactive, Suspended }
#[derive(Debug, PartialEq)]
enum UserTier { Free, Basic, Premium }
#[derive(Debug, PartialEq)] // Added PartialEq for filtering example
struct User {
username: String,
role: UserRole,
status: UserStatus,
tier: UserTier,
}
// Function using matches! directly in if statements
fn process_user_attributes_with_if(user: &User) {
println!("Processing user (matches! in if): {}", user.username);
// Using matches! results directly in if statements - much cleaner!
if matches!(user.role, UserRole::Admin) {
println!(" -> This user is an Admin.");
}
if matches!(user.status, UserStatus::Active) {
println!(" -> This user is currently Active.");
}
// Combining matches! with other conditions
if matches!(user.tier, UserTier::Premium) && user.username.starts_with("admin") {
println!(" -> This user is a Premium Tier user and their username starts with 'admin'.");
}
// Using matches! on a tuple - still concise
if matches!((&user.role, &user.tier), (UserRole::Admin, UserTier::Premium)) {
println!(" -> Specific case: This user is both an Admin AND a Premium Tier user.");
}
// Using matches! with multiple patterns (OR) - elegant syntax
if matches!(&user.status, UserStatus::Suspended | UserStatus::Inactive) {
println!(" -> This user is either Suspended or Inactive. Access may be limited.");
}
println!("---\n"); // Separator for clarity
}
// Function demonstrating using matches! within a closure (for filtering iterators)
fn find_active_premium_users(users: Vec<User>) -> Vec<User> {
println!("Finding active premium users using matches! in a closure...");
// The closure passed to filter uses matches! concisely
let active_premium_users: Vec<User> = users.into_iter()
.filter(|user| {
// matches! provides the boolean needed by filter
matches!((&user.status, &user.tier), (UserStatus::Active, UserTier::Premium))
})
.collect();
println!("Found {} active premium user(s).\n---", active_premium_users.len());
active_premium_users // Return the found users
}
fn main() {
// Create some example users
let admin_active_premium = User { /* ... */ username: "admin_pro".to_string(), role: UserRole::Admin, status: UserStatus::Active, tier: UserTier::Premium };
let regular_inactive_free = User { /* ... */ username: "basic_user".to_string(), role: UserRole::Regular, status: UserStatus::Inactive, tier: UserTier::Free };
let moderator_active_basic = User { /* ... */ username: "mod_user".to_string(), role: UserRole::Moderator, status: UserStatus::Active, tier: UserTier::Basic };
let admin_suspended_basic = User { /* ... */ username: "suspended_admin".to_string(), role: UserRole::Admin, status: UserStatus::Suspended, tier: UserTier::Basic };
let regular_active_premium = User { /* ... */ username: "power_user".to_string(), role: UserRole::Regular, status: UserStatus::Active, tier: UserTier::Premium };
// Process users using the concise function
process_user_attributes_with_if(&admin_active_premium);
process_user_attributes_with_if(®ular_inactive_free);
process_user_attributes_with_if(&moderator_active_basic);
process_user_attributes_with_if(&admin_suspended_basic);
process_user_attributes_with_if(®ular_active_premium);
// Demonstrate using matches! within a closure for filtering
let all_users = vec![
admin_active_premium,
regular_inactive_free,
moderator_active_basic,
admin_suspended_basic,
regular_active_premium, // Move ownership into the vec
];
let _found_users = find_active_premium_users(all_users);
}
/* --- Example User Initialization (Full for copy/paste) ---
let admin_active_premium = User {
username: "admin_pro".to_string(),
role: UserRole::Admin,
status: UserStatus::Active,
tier: UserTier::Premium,
};
let regular_inactive_free = User {
username: "basic_user".to_string(),
role: UserRole::Regular,
status: UserStatus::Inactive,
tier: UserTier::Free,
};
let moderator_active_basic = User {
username: "mod_user".to_string(),
role: UserRole::Moderator,
status: UserStatus::Active,
tier: UserTier::Basic,
};
let admin_suspended_basic = User {
username: "suspended_admin".to_string(),
role: UserRole::Admin,
status: UserStatus::Suspended,
tier: UserTier::Basic,
};
let regular_active_premium = User {
username: "power_user".to_string(),
role: UserRole::Regular,
status: UserStatus::Active,
tier: UserTier::Premium,
};
*/
As shown, the revised process_user_attributes_with_if
function is much cleaner. The boolean checks are performed directly where they're needed, eliminating the intermediate let
bindings and verbose match
blocks from the first example.
If Statements Within Patterns (Match Guards)
To further illustrate the power and elegance of matches!
, let's refine one of the checks from the example above
// Original check combining matches! and an external condition:
if matches!(user.tier, UserTier::Premium) && user.username.starts_with("admin") {
println!(" -> This user is a Premium Tier user and their username starts with 'admin'.");
}
Remember that matches!
supports if
guards within the pattern. We can move the user.username.starts_with("admin")
check directly into the macro call:
// Improved check using an if guard within matches!:
if matches!(user.tier, UserTier::Premium if user.username.starts_with("admin")) {
println!(" -> This user is a Premium Tier user and their username starts with 'admin'.");
}
This version is even more concise and keeps the related conditions tightly coupled within the macro. The expression in the if
guard can be any code that evaluates to a boolean, including function calls or closures.
Also, note how matches!
integrates smoothly into functional programming constructs like the .filter()
method demonstrated in find_active_premium_users
. It provides a natural way to express boolean pattern conditions within closures.
Limitations and Appropriate Usage
While matches!
is incredibly useful, we should be aware of its intended scope and limitations:
Boolean Only: It strictly returns
true
orfalse
.No Value Extraction: It cannot bind values from the pattern like a full
match
orif let
can. If we need to use data within the value being matched,matches!
is not the right tool.Best for Simple Checks: It excels when the primary goal is simply to determine if a value fits a pattern, without needing further action based on the internal data of that pattern.
Macro Overhead: As a macro, it involves code generation during compilation, which might introduce a very small compile-time overhead compared to hand-written
match
statements, though this is usually negligible in practice.
matches!
is best suited for situations where a clear, simple boolean response about whether a value matches a pattern is needed, particularly within conditional logic (if, while)
or functional combinators like filter
.
Summary
The std::matches!
macro offers a significant improvement in code clarity and conciseness for a common task in Rust: checking if a value conforms to a specific pattern without needing to extract data. By providing a clean, standard syntax solely for boolean pattern checks, it reduces the boilerplate often associated with using full match
expressions for this purpose.
While it doesn't replace the power and flexibility of match
or if let
for data extraction, matches!
is a valuable addition to our Rust toolkit, enhancing readability in scenarios demanding simple true/false pattern evaluation.
If you want to see more articles on rust see.