Can irrefutable_let_pattern change to allow bindings that are used later in the if-let chain?

Now that we have let-chains, it is possible to write code like this:

if let my_slice = get_some_slice() && !my_slice.is_empty() {
...

However, this triggers the irrefutable_let_patterns lint. Obviously I can disable this lint, however I think it might be worth considering making the lint a bit narrower. The suggested action in the lint is to move the irrefutable part out of the if, however that's doesn't have the same semantics:

let my_slice = get_some_slice();
if !my_slice.is_empty() {
...

For one thing, it means that my_slice lives longer than the if block, and if there's an else, even if I use braces around the whole block, because of the scope change of if let temporary scope in Edition 2024, this is also semantically different:

{
    let my_slice = get_some_slice();
    if !my_slice.is_empty() {
        ...
    } else { 
        ... // my_slice is still live here, but would not be if I used if let.
    }
}

Now, I'm using a slice for my example because that's what I happened to be using at the time I ran into this, and I know I can do this with pattern matching:

if let my_slice @ [_, ..] = get_some_slice() {

Though I would argue that if let my_slice = get_some_slice() && !my_slice.is_empty() { is potentially clearer even if it is longer.

But let's forget about the slice example, because of course slices don't have interesting drop behavior. Instead, consider this variation on the example from if let temporary scope:

fn example(value: &RwLock<i32>) {
    if let x = *value.read().unwrap() && x >= 3 {
        println!("match {x}");
    } else {
        println!("not match");
        *value.write().unwrap() += 1;
    }
}

(playground)

This seems like a scope usage that would be worth being able to do, both for cases where you really need to drop a guard like this, and just for the semantic value of limiting the scope of a variable to a single if statement.

I don't know how hard it would be to implement, but it seems to me like the irrefutable_let_pattern lint should be narrowed to not apply if the irrefutable binding is used subsequently in the let chain. That is, if you want something scoped to an if block, but don't use it in a let chain, you can move it inside the if statement, but if you

// irrefutable_let_patterns would still apply to this:
if some_bool && let scoped_binding = source() {
}
// Since you could just do this instead:
if some_bool {
    let scoped_binding = source();
}

// But if you do use the binding in the let chain, you obviously 
// can't move it inside:
if let scoped_binding = source() && scoped_binding.some_bool() {
    // Too late to declare scoped_binding here since it's needed 
    // in the condition.

This didn't make sense before if-let chains, since there wasn't previously a way to use a scoped binding in the conditional like this. Being able to use if-let chains like this would give Rust the power of Go's if with declaration.

3 Likes

I'd like to throw in that the semantics preserving mechanical solution (that, arguably should be what is suggested) is something like:

if let Some(x) = Some(*value.read().unwrap()).filter(|x| x >= 3)
// or maybe something with more braces?

But I don't know how I feel about this change in general. Potentially it disables the lint in cases where the let pattern is erroneously irrefutable but the value is nevertheless used (can't think of a good example), in which case it might be clearer to just live with

#[allow(irrefutable_let_patterns)]
if let x = *value.read().unwrap() && x >= 3 {
6 Likes