Chapter 17. Patterns and syntax

Up through this point we've been using quite a bit of Rust's pattern and corresponding matching syntax without explicitly defining under what circumstances this syntax is leveragable.

In this chapter we'll take a deeper look into Rust's pattern and syntax to give you some practical examples and additional context for when and how to usefully use it.

Chapter 17. Patterns and syntax

The human brain is an incredible pattern-matching machine. - Jeff Bezos

Reintroducing DisplayAd

Let's take our DisplayAd struct from earlier in the text, which we'll use to demonstrate a useful feature of pattern matching called destructuring. As a reminder, here is how we defined it earlier.

#[derive(Debug)] // this is for printing using "{:?}" or "{:#?}"
struct DisplayAd {
    start_timestamp: i64,
    budget: u32,
    title: String,
    copy: String,
    call_to_action: String,
    media_asset_urls: Vec<String>,
    button_text: String,
    target_url: String,
}

And let's fill it with some data and instantiate it:

fn main() {
    let my_display_ad = DisplayAd {
        start_timestamp,
        budget: 5000,
        title: String::from("My first ad"),
        copy: String::from("Buy whatever I'm selling. It's great!"),
        call_to_action: String::from("On sale today only!"),
        button_text: String::from("Buy now"),
        target_url: String::from("https://tincre.com/agency"),
        media_asset_urls: vec![String::from("https://res.cloudinary.com/tincre/video/upload/v1708121578/nfpwzh1oslr8qhdyotzs.mov")],
    };
}

Destructuring structs

Destructuring is a modern language feature you might have come across in Python, JavaScript or other language. It's the process of simply grabbing the parameters from a struct using a single syntax to do so, for example:

let start_timestamp: i64 = 1721752441;

let DisplayAd {
    start_timestamp,
    budget,
    title: "This doesn't get a proper name",
    copy,
    call_to_action,
    button_text,
    target_url,
    media_asset_urls,
} = my_display_ad;

In the above, printing budget with println!("{}", budget) will print 2000 to standard out, the original value to budget we set. That said, normal variable shadowning applies here; title above is set to "This doesn't get a proper name".

Destructuring enums

Rust developers can also use destructuring with enums. Let's consider a the Ad enum for our DisplayAd struct we defined earlier.

Let's assume for a moment that our DisplayAd struct now has a property called platform, an enum that enumerates available ad platforms. Here's what that might look like.

enum Platform {
    Meta,
    Google,
    TikTok,
    Minecraft,
    Uber,
    SnapChat,
    Twitter,
    CampaignDates(String, String)
}

When using this Platform enum we can destructure the values of CampaignTimes, a tuple-like enum variant, e.g.

fn main() {
    let platform = Platform::CampaignDates("2024-09-25".to_string(), "2024-10-05".to_string());

    match platform {
        Platform::CampaignDates(start_date, end_date) => {
            println!("This campaign start date is {start_date} and end date is {end_date}");
        }
        _ => {},
    }
}

When you run the above you should see something like the below print to your console.

This campaign start date is 2024-09-25 and end date is 2024-10-05.

So what if we have a variant in our enum that's struct-like? How about adding a field for a target link that holds both a short link and the original link?

Note: Some ad platforms don't allow short links while some do, so it is probably a good idea to include both in each platform!

enum Platform {
    Meta,
    Google,
    TikTok,
    Minecraft,
    Uber,
    SnapChat,
    Twitter,
    CampaignDates(String, String),
    TargetLink {
        short_link: String,
        original_link: String,
    },
}

And we can use it in a new match statement, e.g.

fn main() {
    let platform = Platform::TargetLink {
        short_link: "https://tinc.re/xyzabc".to_string(),
        original_link: "https://tincre.com".to_string(),
    };

    // adding to our earlier match...
    match platform {
        Platform::CampaignDates(start_date, end_date) => {
            println!("This campaign start date is {start_date} and end date is {end_date}");
        }
        Platform::TargetLink {
            short_link: short,
            original_link: orig,
        } => {
            println!("The short link {short} and raw link {orig} are set.");
        }
        _ => {}
    }
}

But what if one of our enum variants is nested? For example, the Meta variant could be updated to be a variant of type MetaPlatform, e.g.

enum MetaPlatform {
    InstagramFeed,
    FacebookFeed,
    InstagramReels,
    FacebookStory,
    QuestVR,
}

enum Platform {
    Meta(MetaPlatform),
    Google,
    TikTok,
    Minecraft,
    Uber,
    SnapChat,
    Twitter,
    CampaignDates(String, String),
    TargetLink {
        short_link: String,
        original_link: String,
    },
}

We might want to do this so as to restrict ads to specific, enumerable types of platforms depending on the parent platform.

Now let's see it used in action:

fn main() {
    let platform = Platform::Meta(MetaPlatform::InstagramFeed);

    match platform {
        Platform::Meta(MetaPlatform::InstagramFeed) => {
            println!("Running on instagram feed!");
        }
        Platform::Meta(MetaPlatform::FacebookFeed) => {
            println!("Running on facebook feed!");
        }
        _ => {}
    }

}

Other places where patterns exist

Functions and ignoring values are two other places where patterns are used in Rust. Let's look at each of these, briefly, since they follow the same rules as above.

Functions

Function parameters use patterns in the Rust language, too.

fn use_pattern((a, b): (String, String)) {
    println!("a is {a} and b is {b}");
}

fn main() {
    // tuple destructuring
    let (a, b) = (1, 2);
    println!("a is {a} and b is {b}");

    use_pattern(("1".to_string(), "2".to_string()));
}

In the function use_pattern above we're using a tuple pattern to destructure the tuple into two separate variables, in the input to the function.

This is a particularly useful language features for passing the same type of value through many different functions. For example, building web APIs that return tRPC or JSON responses can be strongly typed and enforced reliably and safely.

Ignoring values

Rust also allows you to ignore values using the _ character. This is useful when you're not interested in a value, but you need to match the pattern.

This is something you'll see often throughout Rust codebases. Let's look at the most simple version, a match catchall.

let x = 10;

match x {
    1 => println!("One"),
    _ => println!("Something else"),
}

Because the _ character is a catchall, it matches any value that isn't 1 in the above example.

Rust also lets us use .. in several places. For example, while matching a struct, we can use it to ignore all the other fields in the struct.

struct ThreeDPoint {
    x: f32,
    y: f32,
    z: f32,
}

fn main() {
    let point = ThreeDPoint { x: 1.0, y: 2.0, z: 3.0 };

    match point {
        ThreeDPoint { x, .. } => println!("x is {x}"),
    }
}

As long as .. is not ambiguous, it can be placed anywhere in the pattern.

Irrefutable and refutable patterns

We can break down patterns into two forms: irrefutable and refutable, meaning those that match for all values and those patterns where some value can fail to match, respectively.

Though active distinction between the two types isn't necessarily useful for writing software, you'll want familiarity so that you can track down bugs from the compiler's error messages.

struct ThreeDPoint {
    x: f32,
    y: f32,
    z: f32,
}

fn main() {
    let point = ThreeDPoint {
        x: 1.0,
        y: 2.0,
        z: 3.0,
    };

    let Some(my_point): Option<ThreeDPoint> = Some(point);
}

You should notice that this doesn't compile. In my editor, I get the following error:

non-exhaustive pattern: `None` not covered (rust-analyzer E0005)
──────────────────────────────────────────────────────────────────────────────
https://doc.rust-lang.org/stable/error_codes/E0005.html
──────────────────────────────────────────────────────────────────────────────
refutable pattern in local binding
`let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant

Meaning let bindings need to be able to accept all values. The Option<ThreeDPoint> won't work for let.

Function parameters and for loops are similar to let - irrefutable patterns are required. Everything else can match to refutable patterns.

Fixing our code above, we use an if let statement which you've seen previously:

#[derive(Debug)]
struct ThreeDPoint {
    x: f32,
    y: f32,
    z: f32,
}

fn main() {
    let point = ThreeDPoint {
        x: 1.0,
        y: 2.0,
        z: 3.0,
    };

    if let Some(my_point) = Some(point) {
        println!("We have a point {:?}!", my_point);
    };
}

@ binding syntax

Though we're not exhaustively reviewing all Rust matching syntax here, one particularly useful part of Rust's feature set is the @ binding.

Using this binding we can both test for validity and assign to a variable at the same time. For example, let's revisit our ThreeDPoint class and make the assumption that z should be a unit vector (between 0 and 1). The @ binding syntax makes this extremely easy and readable:

fn main() {
    let point = ThreeDPoint {
        x: 1.0,
        y: 2.0,
        z: 3.0,
    };

    match point {
        ThreeDPoint {
            x: new_x,
            y: new_y,
            z: new_z @ 0.0..1.0, // "bind" z between 0.0 and 1.0
        } => println!(
            "{:?} is valid",
            ThreeDPoint {
                x: new_x,
                y: new_y,
                z: new_z
            }
        ),
        _ => println!("{:?} is not valid", point),
    }
}

Running the above will tell us that our ThreeDPoint is not valid. Change z to something between 0.0 and 1.0 (inclusive) and running the code will tell us that we have a valid ThreeDPoint.

Magic, right?

Was this page helpful?