To bind or not to bind

2 minute read Published: 2022-10-14

Binding or Name Binding is a way to associate data with a symbolic identifier usually called variable. Why it is important in Rust, what rocks are underwater and how it intersects with scopes and lifetimes?

Scopes

Variable Binding is type safe in Rust. In most cases you only need to define a name with binding to an initalized value for type being inferred automatically by the compiler.

When you define a variable and bind data to it - lifitime of it takes a start point until variable goes out of scope:

fn main() {
|-a    let variable_a = "A";
|      {        
|       |-b  let variable_b = "B";
|       |    println!("{}", variable_a);
|       |--  println!("{}", variable_b);
|      }
|      println!("{}", variable_a);
|--    println!("{}", variable_b); //ERROR - variable_b is not in scope
}

As you can see in example we can't use variable if scope where it's data was binded to it ended.

Let it _be

Why am I writing about such basics aspects of Rust? Just to share you one example where it comes tricky.

If variable won't be called later in our program, idiomatic way to tell it's okay to compiler will be to add _ prefix to variable's name: let _result = io::send_request();

And we can assume that having binding to variable with a _ name would have the same effect, which is not a case in the following example. Let's look into code first and then discuss what's happened.

`_pool``_`
fn main() {
    let mut database_pool = Some(vec![]);
    if let Some(_pool) = database_pool {
        println!("Already initialized");
    }
    initialize_pool(database_pool);
}

fn initialize_pool(pool: Option<Vec<i32>>) {
    todo!();
}
fn main() {
    let mut database_pool = Some(vec![]);
    if let Some(_) = database_pool {
        println!("Already initialized");
    }
    initialize_pool(database_pool);
}

fn initialize_pool(pool: Option<Vec<i32>>) {
    todo!();
}
error[E0382]: use of partially moved value:
 `database_pool`
 --> src/main.rs:6:21
  |
3 |     if let Some(_pool) = database_pool {
  |                 - value partially moved here
...
6 |     initialize_pool(database_pool);
  |                     ^ value used here after
   partial move
  |
  = note: partial move occurs because value has
   type `Vec<i32>`,
   which does not implement the `Copy` trait
help: borrow this field in the pattern to avoid
 moving `database_pool.0`
  |
3 |     if let Some(ref _pool) = database_pool {
  |                 +++
Already initialized

So why does these two examples behave differently? According to the Book _variable_name binding is still a binding which will just avoid "unused variable" warning. When _ won't bind it at all.

fn main() {
    let mut database_pool = Some(vec![]);
    let _ = database_pool;
    initialize_pool(database_pool);
}

fn initialize_pool(pool: Option<Vec<i32>>) {
    todo!();
}

This code above will compile and work properly while code below:

fn main() {
    let mut database_pool = Some(vec![]);
    let _database_pool = database_pool;
    initialize_pool(database_pool);
}

fn initialize_pool(pool: Option<Vec<i32>>) {
    todo!();
}

won't compile at all.

Conclusion

Rust is not a Javascript, but it still tricky sometimes. Did you know and notice this behaviour before?