Destructuring

Last time we looked at Rust's data types. Once you have some data structure, you will want to get that data out. For structs, Rust has field access, just like C++. For tuples, tuple structs, and enums you must use destructuring (there are various convenience functions in the library, but they use destructuring internally). Destructuring of data structures doesn't happen in C++, but it might be familiar from languages such as Python or various functional languages. The idea is that just as you can create a data structure by filling out its fields with data from a bunch of local variables, you can fill out a bunch of local variables with data from a data structure. From this simple beginning, destructuring has become one of Rust's most powerful features. To put it another way, destructuring combines pattern matching with assignment into local variables.

Destructuring is done primarily through the let and match statements. The match statement is used when the structure being desctructured can have difference variants (such as an enum). A let expression pulls the variables out into the current scope, whereas match introduces a new scope. To compare:

fn foo(pair: (int, int)) {
    let (x, y) = pair;
    // we can now use x and y anywhere in foo

    match pair {
        (x, y) => {
            // x and y can only be used in this scope
        }
    }
}

The syntax for patterns (used after let and before => in the above example) in both cases is (pretty much) the same. You can also use these patterns in argument position in function declarations:

fn foo((x, y): (int, int)) {
}

(Which is more useful for structs or tuple-structs than tuples).

Most initialisation expressions can appear in a destructuring pattern and they can be arbitrarily complex. That can include references and primitive literals as well as data structures. For example,

struct St {
    f1: int,
    f2: f32
}

enum En {
    Var1,
    Var2,
    Var3(int),
    Var4(int, St, int)
}

fn foo(x: &En) {
    match x {
        &Var1 => println!("first variant"),
        &Var3(5) => println!("third variant with number 5"),
        &Var3(x) => println!("third variant with number {} (not 5)", x),
        &Var4(3, St { f1: 3, f2: x }, 45) => {
            println!("destructuring an embedded struct, found {} in f2", x)
        }
        &Var4(_, x, _) => {
            println!("Some other Var4 with {} in f1 and {} in f2", x.f1, x.f2)
        }
        _ => println!("other (Var2)")
    }
}

Note how we destructure through a reference by using & in the patterns and how we use a mix of literals (5, 3, St { ... }), wildcards (_), and variables (x).

You can use _ wherever a variable is expected if you want to ignore a single item in a pattern, so we could have used &Var3(_) if we didn't care about the integer. In the first Var4 arm we destructure the embedded struct (a nested pattern) and in the second Var4 arm we bind the whole struct to a variable. You can also use .. to stand in for all fields of a tuple or struct. So if you wanted to do something for each enum variant but don't care about the content of the variants, you could write:

fn foo(x: En) {
    match x {
        Var1 => println!("first variant"),
        Var2 => println!("second variant"),
        Var3(..) => println!("third variant"),
        Var4(..) => println!("fourth variant")
    }
}

When destructuring structs, the fields don't need to be in order and you can use .. to elide the remaining fields. E.g.,

struct Big {
    field1: int,
    field2: int,
    field3: int,
    field4: int,
    field5: int,
    field6: int,
    field7: int,
    field8: int,
    field9: int,
}

fn foo(b: Big) {
    let Big { field6: x, field3: y, ..} = b;
    println!("pulled out {} and {}", x, y);
}

As a shorthand with structs you can use just the field name which creates a local variable with that name. The let statement in the above example created two new local variables x and y. Alternatively, you could write

fn foo(b: Big) {
    let Big { field6, field3, .. } = b;
    println!("pulled out {} and {}", field3, field6);
}

Now we create local variables with the same names as the fields, in this case field3 and field6.

There are a few more tricks to Rust's destructuring. Lets say you want a reference to a variable in a pattern. You can't use & because that matches a reference, rather than creates one (and thus has the effect of dereferencing the object). For example,

struct Foo {
    field: &'static int
}

fn foo(x: Foo) {
    let Foo { field: &y } = x;
}

Here, y has type int and is a copy of the field in x.

To create a reference to something in a pattern, you use the ref keyword. For example,

fn foo(b: Big) {
    let Big { field3: ref x, ref field6, ..} = b;
    println!("pulled out {} and {}", *x, *field6);
}

Here, x and field6 both have type &int and are references to the fields in b.

One last trick when destructuring is that if you are detructuring a complex object, you might want to name intermediate objects as well as individual fields. Going back to an earlier example, we had the pattern &Var4(3, St{ f1: 3, f2: x }, 45). In that pattern we named one field of the struct, but you might also want to name the whole struct object. You could write &Var4(3, s, 45) which would bind the struct object to s, but then you would have to use field access for the fields, or if you wanted to only match with a specific value in a field you would have to use a nested match. That is not fun. Rust lets you name parts of a pattern using @ syntax. For example &Var4(3, s @ St{ f1: 3, f2: x }, 45) lets us name both a field (x, for f2) and the whole struct (s).

That just about covers your options with Rust pattern matching. There are a few features I haven't covered, such as matching vectors, but hopefully you know how to use match and let and have seen some of the powerful things you can do. Next time I'll cover some of the subtle interactions between match and borrowing which tripped me up a fair bit when learning Rust.