Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support && in if let expressions #929

Closed
mdinger opened this issue Mar 3, 2015 · 26 comments
Closed

Support && in if let expressions #929

mdinger opened this issue Mar 3, 2015 · 26 comments
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@mdinger
Copy link
Contributor

mdinger commented Mar 3, 2015

Update:

@kballard has a better suggestion than I did at #929 (comment) . Similar type of idea but borrows from Swift.

Original idea below.


Seems like a good idea to support && in if let expressions. I'm not sure about ||. It seems good from a consistency standpoint but I'm not sure if the the fact that the destructuring could result in different types could present a problem or not. In #525 it was a problem so I'd anticipate a problem here as well.

fn main() {
    // `op_op` (option option)
    let op_op = Some(Some(6));

    // Leads to deep nesting which is bad.
    if let Some(op) = op_op {
        if let Some(i) = op {
            println!("Matched {:?}, {:?}!", op, i);
        }
    }

    // It'd be nice to support the `&&` operator. `op_op` destructures
    // then `op` destructures. These would be directly equivalent.
    if let Some(op) = op_op &&
           Some(i) = op {
        println!("Matched {:?}, {:?}!", op, i);
    }

    // With `else`
    if let Some(op) = op_op {
        if let Some(i) = op {
            println!("Matched {:?}, {:?}!", op, i);
        } else {
            println!("Didn't");
        }
    } else {
        println!("Didn't");
    }

    // Would be replaced with
    if let Some(op) = op_op && 
           Some(i) = op {
            println!("Matched {:?}, {:?}!", op, i);
    } else {
        println!("Didn't");
    }

    let (op1, op2) = (Some(7), Some(8));

    // If `&&` is allowed, `||` shoud be also. Take first branch that
    // destructures. Otherwise do nothing.
    if let Some(i) = op1 ||
           Some(i) = op2 {
        println!("Matched {:?}!", i);
    }

    // Is equivalent to:
    if let Some(i) = op1 {
        println!("Matched {:?}!", i);
    else if let Some(i) = op2 {
        println!("Matched {:?}!", i);
    }

    // This is invalid because either `i` or `j` might appear in the
    // expression. The destructuring must use the same identifiers.
    if let Some(i) = op1 ||
           Some(j) = op2 {
        println!("Matched {:?}!", i);
    }

    // Adding an `else` clause:
    if let Some(i) = op1 {
        println!("Matched {:?}!", i);
    else if let Some(i) = op2 {
        println!("Matched {:?}!", i);
    } else {
        println!("Didn't");
    }

    // Would be equivalent to:
    if let Some(i) = op1 ||
           Some(i) = op2 {
        println!("Matched {:?}!", i);
    } else {
        println!("Didn't");
    }

    // These can go really deep:
    let op_by_4 = Some(Some(Some(Some(6))));

    // Would be either:
    if let Some(op_by_3) = op_by_4 {
        if let Some(op_by_2) = op_by_3 {
            if let Some(op) = op_by_2 {
                if let Some(i) = op {
                    println!("Matched {:?}, {:?}, {:?}, {:?}, {:?}!",
                              op_by_4, op_by_3, op_by_2, op, i);
                }
            }
        }
    }

    if let Some(op_by_3) = op_by_4 &&
           Some(op_by_2) = op_by_3 &&
           Some(op) = op_by_2 &&
           Some(i) = op {
        println!("Matched {:?}, {:?}, {:?}, {:?}, {:?}!",
                 op_by_4, op_by_3, op_by_2, op, i);
    }
}

Technically, && is typically used with bool so & or some other identifier (and?) could be valid here as well. I thought && made sense though because this is being interpreted in a slightly similar fashion to a boolean expression.

EDIT: Added else to examples and an example of a deeper nesting example.

@Marwes
Copy link

Marwes commented Mar 9, 2015

I don't see how '&&' adds anything since you can nest patterns, i.e.

    if let Some(Some(i)) = op_op {
        println!("Matched {:?}, {:?}!", op, i);
    }

'||' is basically there as well but its through '|' though it doesn't work for 'if let' yet.

@mdinger
Copy link
Contributor Author

mdinger commented Mar 9, 2015

@Marwes Sorry. Maybe I simplified too much. The idea was to make this reddit problem a little nicer. In his case, he has more different enum variants but every else statement is essentially the same (I'm not sure they could collapse to a single else condition but I think they might. I looked at it more a few days ago).

Let me know if this example is better. I can modify the header problem then.

enum Enum {
    Null,
    Cons(Box<Enum>),
}

fn main() {
    let deep = Enum::Cons(Box::new(
               Enum::Cons(Box::new(
               Enum::Cons(Box::new(
               Enum::Null))))));

    // There are two issues. First is that nesting requires a dereference
    // every time. The second is that when structured like this, every `if let`
    // requires a separate `else` condition even if they are all identical.
    if let Enum::Cons(far_outer) = deep {
        if let Enum::Cons(outer) = *far_outer {
            if let Enum::Cons(inner) = *outer {
                if let Enum::Null = *inner {
                    println!("Null!");
                } else { println!("Failed!") }
            } else { println!("Failed!") }
        } else { println!("Failed!") }
    } else { println!("Failed!") }

    // This would avoid them.
    if let Enum::Cons(far_outer) = deep &&
           Enum::Cons(outer) = *far_outer &&
           Enum::Cons(inner) = *outer &&
           Enum::Null = *inner {
                    println!("Null!");
    } else { println!("Failed!") }    
}

@mdinger
Copy link
Contributor Author

mdinger commented Mar 9, 2015

Also, | is being discussed in #935 but it's being used differently:

fn main() {
    let three = Some(3);
    let four = Some(4);

    // `|` is being used on alternations of a *single* destructure.
    // Destructure `three` and see if it matches either available
    // alternatives
    if let Some(3) | Some(4) = three { println!("Found 3 or 4"!) }

    // `||` is being used on *different* destructures of different variables.
    // Try to destructure `three` and if it fails, then try destructuring a
    // different variable `four` to determine if it matches a second condition.
    if let Some(3) = three ||
           Some(4) = four { println!("Found 3 or 4!") }

}

@mdinger
Copy link
Contributor Author

mdinger commented Mar 12, 2015

I discovered it's possible to implement deref which may mitigate the issue though I'm not sure it could be used in the reddit case. This idea may be useful still. I'm not sure deref could fix all cases.

use std::ops::Deref;

#[derive(Debug)]
enum Enum {
    Num(End),
    Cons(Box<Enum>),
}

#[derive(Debug)]
struct End(i32);

impl Deref for Enum {
    type Target = End;

    fn deref(&self) -> &End {
        match *self {
            Enum::Cons(ref b) => b.deref(),
            Enum::Num(ref i) => i,
        }
    }
}

fn main() {
    let deep = Enum::Cons(Box::new(
               Enum::Cons(Box::new(
               Enum::Num(End(3))))));

    match *deep {
        End(i) => println!("Got `i`: {}", i),
    }
}

@lilyball
Copy link
Contributor

if let came from Swift, and Swift 1.2 adds a syntax:

if let a = foo(), let b = a.bar(), let c = b.baz() where c > 2, let d = c.quux() {
    // all operations succeeded
} else {
    // something failed
}

(which actually can be shortened slightly, the 2nd and 3rd lets are unnecessary, it's only necessary on the first one and after a where clause, but I put it in there for clarity)

I'm inclined to say we should do something similar. I think this syntax makes more sense than trying to conceptually overload || and &&, especially since there's a conflict there where || and && are real operators and therefore would be parsed as part of the expression, but the comma is currently meaningless at that position.

@mdinger
Copy link
Contributor Author

mdinger commented Mar 16, 2015

That's better than my idea.

@ftxqxd
Copy link
Contributor

ftxqxd commented Mar 16, 2015

A more general feature that would allow this would be making PATTERN if let PATTERN = EXPR a pattern itself, sort of like a guard but for if let. That could even be used outside if let:

match foo() {
    Some(i) if let Some(num) = i.parse::<i32>() => ...,
    Some(i) if let Some(num) = i.parse::<f64>() => ...,
    ...
}

Used inside if let:

if let (((a if let b = a.bar()) if let c = b.baz()) if let d = c.quux()) = foo() {
    // all operations succeeded
} else {
    // something failed
}

Although that is a lot less readable than @kballard’s syntax, it’s more general. I’d probably prefer having both features: if let guards for flexible patterns everywhere, and the comma syntax as syntactic sugar for something like the above.

@lilyball
Copy link
Contributor

@P1start it's more general, but as you say, it's less readable. I'm also concerned that it would not interact well with the restriction that you cannot bind by-move into a pattern guard. Note that this restriction is in place even if no other patterns attempt to bind the value in question. That restriction means you cannot replicate Swift 1.2's if let a = foo(), b = a with this proposed if the value is a by-move.

That said, I'm inclined to say that maybe we should allow if let in a pattern guard anyway, because it can be convenient in some cases, such as in your example (since numbers are not by-move). But we should also think about supporting Swift 1.2's syntax. We just won't be able to turn the enhanced if let syntax into pattern guards like you're suggesting.

@NeoLegends
Copy link

I want this. I've got quite some cases where I need intermediate processing between the destructuring (which is why the nested patterns don't apply so much) or need multiple results from destructuring at once. In those cases, the nests go really deep.

@djrenren
Copy link

djrenren commented Jul 12, 2016

Just ran into this issue myself. It would be immensely useful. In fact, as evidence of the usefulness (and feasibility) of this feature. Check out Manishearth/rust-clippy which utilizes a macro called if_let_chain! quite extensively:
https://github.com/Manishearth/rust-clippy/blob/ad1cd990549fdfc8ae9dcd4ea7eea851017eb042/clippy_lints/src/utils/higher.rs#L122

@SoniEx2
Copy link

SoniEx2 commented Sep 9, 2016

Another option would be making let PATTERN = EXPRESSION an expression itself, returning a bool, true if the pattern matched, false otherwise. It'd bind values only during the current expression/statement so e.g.

let x = let Some(_) = option;

would make x true if option was Some, false otherwise, and e.g.

let x = let Some(b) = option && !b;

would make x true if option was Some(false), or false otherwise. You get the idea.

For ||, you'd have to bind the same name on both sides, just like with | in match, and they'd have to be the same types. As an example:

if let Some(x) = option || let x = default { x }

is equivalent to option.unwrap_or(default).

Etc. This requires much less special casing so it can just work everywhere. And the compiler can warn about let Some(x) = option; outside expressions.

@czakian
Copy link

czakian commented Oct 13, 2016

I would also like this feature.

@aleics
Copy link

aleics commented Nov 14, 2016

this would be great!

@ajdlinux
Copy link

Just ran into this today. I really like the Swift syntax.

@jhaberstro
Copy link

This is my first day using Rust and I already ran into the need for this feature.. :)
👍

@algesten
Copy link

Another solution is the "swift guard" style statement, it seems to be all the rage in the swift community.

guard let x = x else {return false}
// x is now more > 0

For Rust that could look something like:

guard let Ok(mut reader) = fs::File::open(&filename) else {return false};
// reader is now defined

@glaebhoerl
Copy link
Contributor

@algesten See also #373, #1303

@BattyBoopers
Copy link

The feature is already there basically, because Rust has tuples:

    let x : Option<&str> = Some("a");
    let y : Option<&str> = Some("b");

    if let (Some(a),Some(b)) = (x,y) {
        println!("got a={}, b={}",a,b);
    }
    else {
        println!("a and b are not both defined");
    }

@mdinger
Copy link
Contributor Author

mdinger commented Jan 26, 2017

This is a misunderstanding of the main linked post and is hardly equivalent. It completely lacks the ability to apply functionality to destructured types and then continue to destructure them without entering a new block. The original post would have shared these abilities but doesn't have as clean a form.

@comex
Copy link

comex commented Mar 10, 2017

It looks like C# 7.0 supports the equivalent of 'let' in an expression:

if (o is int i || (o is string s && int.TryParse(s, out i)) { /* use i */ }

So it's not completely insane.

If let a = b became an expression (returning bool) in Rust, combined with the existing variable initializedness checks, I think both && and if !let (aka guard let) would mostly just work. One giant caveat is scoping: for normal if lets you want the binding to be visible only inside the 'then' block, while for if !let the binding needs to last for the block containing the if. I don't know if there's a good way to solve this.

@petrochenkov
Copy link
Contributor

petrochenkov commented Mar 10, 2017

It looks like C# 7.0 supports the equivalent of 'let' in an expression:

The example @comex gave is exactly what I'd like non-exhaustive pattern matching to look in Rust

if opt_x is Some(x) && x > 10 {
    println!("{}", x);
}

and adding this to if let is like trying to heal a dead man.
I wanted to write a pre-RFC for this for years, but there always was something more important.


Proof-of-concept implementation is available on my branch!
https://github.com/petrochenkov/rust/tree/isproto
The feature was also tested on rustfmt crate.
https://github.com/petrochenkov/rustfmt/tree/isproto
More detailed description: #2260 (comment).

@gaurikholkar-zz
Copy link

Hey, any updates on this?

@I60R
Copy link

I60R commented Nov 28, 2017

Ran into this on my first days programming in Rust. Here are my thoughts in hope of future improvements.


So, currently Rust has two kinds of if expressions: one that's used with boolean conditions and another that's used for matching/destructuring.

First problem: that's weird because both ifs uses the same keyword but are "incompatible", they must be nested instead.
Second problem: matching/destructuring version can process only single pattern which causes nesting if you have a lot of patterns.
Third problem: matching/destructuring version of if don't have negation and is "assymetric" with boolean version


For the first:
Compatibility between two versions of ifs can be provided if Rust will allow to gluing them.
I'm inspired by @P1start post:

    if boolean_flag if let Some(thing) = get_it() { ... } else { ... }
    if let Some(thing) = get_it() if boolean_flag { ... } else { ... }

That seems to be the most "rusty" way because similar syntax yet possible in match expression.
That also could make expressions like:

    if is_a()
    if is_b() {
        ...
    }

    if let Ok(a) = try_get_a()
    if let Ok(b) = try_get_b() {
        ...
    }

completely valid, which from my point of view don't look so bad.
Even it can improve readability serving as a separator in complex boolean checking logic.

    if (get_a() && get_b()) || get_c()
    if is_something_valid()
    if let Some(x) = get_it() {
        ...
    }

Code formatters could keep these ifs on a new line without adding extra spaces (I never liked when && was re-aligned).


For the second:
As stated @kballard multiple patterns on matching/destructuring ifs can be separated with comma

    if let
        Some(a) = get_opt_a(),
        Ok(b) = try_get_b(a),
        Some(c) = local_value {
        ...
    }

I'm against reusing && for same reasons.
I'm also against let $PATTERN$ = $EXPRESSION$ to return bool because it could be possible to write let a = let Some(b) = expr() which is weird.
I'm also against || because .or_else (or equivalent extensions) can serve the same purpose.
guard, is, etc. also doesn't make sense when existed syntax can be effectively reused.


For the third:
I would see ! before type rather than before let. Then it will be to possible to use it on multiple if let patterns.
Also compiler should enforce _ on negated type to not bring nonsense variables into scope:

    if let
        !Err(_) = try_prepare(),
        Ok(something) = try_get() {
        ...
    }

@Centril Centril added the T-lang Relevant to the language team, which will review and decide on the RFC. label Dec 24, 2017
@SoniEx2
Copy link

SoniEx2 commented Feb 12, 2018

"Error" handling could be made pretty (ref #929 (comment)):

(let (Some(a), Some(b)) = (x, y)) || {
    println!("failed to unpack x and y");
    return;
};
// use `a` here, without causing an additional level of indentation/nesting.

(The alternative being

let a;
let b;
if let (Some(x1), Some(y1)) = (x, y) {
    a = x1;
    b = y1;
} else {
    println!("failed to unpack x and y");
    return;
}
// use `a` here, without causing an additional level of indentation/nesting.

Which is just... so much more verbose that I'd much rather have the former.)

@petrochenkov
Copy link
Contributor

Notifying on this thread as well: proof-of-concept implementation of #929 (comment) is available, see #2260 (comment) for detailed description.

@Centril
Copy link
Contributor

Centril commented Oct 7, 2018

Closing in favor of accepted RFC #2497.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

No branches or pull requests