Better ergonomics for match guards (error E0008)

Error E0008 seems overly restrictive. It prohibits using variables captured by value in a match arm in a guard expression (unless, it seems, the value implements Copy).

It does make sense that the guard expression shouldn’t be able to take ownership of the value, because then not only would you not be able to use the value in the body of the branch, but you wouldn’t be able to check any later branches either, since the value would already be consumed.

However, I don’t see why you can’t use a guard expression that only needs a reference.

Consider this:

enum Test {
    A(i32),
    B(String)
}

fn main() {
    use Test::*;
    let x = B("Hello!".into());
    match x {
       A(y) => println!("Got an int: {}", y),
       B(s) if s.len() > 0 => println!("Got string with bytes: {:?}", s.into_bytes()),
       B(_) => println!("Got an empty string")
    }
}

In the second arm, we check the length of the string, but since s is captured by value, we get error E0008, even though the guard doesn’t actually need ownership. The description of the error gives the solution of capturing by ref, and if you need ownership, clone. However, this doesn’t always work. What if the body of the branch needs ownership of the original value and a clone doesn’t work, or is expensive.

There is a another workaround (not mentioned in the erro index):

match x {
  A(y) => ...
  B(s) => if s.len() > 0 {
    ... 
  } else {
    ...
  }

And in this case, it isn’t too bad, but if the matching is more complicated, it can get kind of messy (ex requiring a nested match inside the else clause).

What’s even more suprising, is even this results in the error:

match x {
   Some(s) if true => {}
   None => {}
}

Even if the binding isn’t used in the guard expression at all it is an error. This is very surprising.

What I propose is that if a value is captured by value and doesn’t implement Copy (or whatever the rule is that triggers this error), then the variable should be bound by reference for the guard expression and ownership for the body.

3 Likes

This is definitely a place in Rust that feels strangely unpolished – why do I have to move my binding when I use a match guard?

Is there a design reason that references haven’t been tackled, maybe?

1 Like

We need to take care that immutable references to types with interior mutability aren’t allowed. Otherwise the guards could modify some state, which I’m assuming is something we really don’t want.

This can already happen.

use std::cell::RefCell;

fn main() {
    let x = RefCell::new(false);
    match x {
        ref y if y.replace(true) => println!("branch 1"),
        ref y if y.replace(true) => println!("branch 2"),
        _ => println!("default branch"),
    }
}
3 Likes

The existing handling of matches and the borrow checker is a bit kludgey. The MIR-based borrow checker handles these things quite a bit differently (though this work is not yet 100% complete). This simultaneously closes some soundness bugs with the existing system and enables new usage patterns -- for example, we do allow you to have if guards even when a binding "moves" values out of the matched value (however, the guard is not allowed to move those values -- it effectively has only borrowed access to them).

(Actually, the PR with those new semantics hasn't landed yet I don't think.)

3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.