Skip to content

Support && in if let expressions #929

Description

@mdinger

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.

Activity

  1. Marwes commented on Mar 9, 2015

    @Marwes

    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.

  2. mdinger commented on Mar 9, 2015

    @mdinger
    ContributorAuthor

    @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!") }    
    }
  3. mdinger commented on Mar 9, 2015

    @mdinger
    ContributorAuthor

    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!") }
    
    }
  4. mdinger commented on Mar 12, 2015

    @mdinger
    ContributorAuthor

    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),
        }
    }
  5. lilyball commented on Mar 16, 2015

    @lilyball
    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.

  6. mdinger commented on Mar 16, 2015

    @mdinger
    ContributorAuthor

    That's better than my idea.

  7. ftxqxd commented on Mar 16, 2015

    @ftxqxd
    Contributor

    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.

  8. lilyball commented on Mar 16, 2015

    @lilyball
    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.

  9. NeoLegends commented on May 26, 2016

    @NeoLegends

    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.

  10. djrenren commented on Jul 12, 2016

    @djrenren

    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

  11. SoniEx2 commented on Sep 9, 2016

    @SoniEx2

    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.

  12. czakian commented on Oct 13, 2016

    @czakian

    I would also like this feature.

  13. aleics commented on Nov 14, 2016

    @aleics

    this would be great!

  14. ajdlinux commented on Nov 21, 2016

    @ajdlinux

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

  15. jhaberstro commented on Nov 24, 2016

    @jhaberstro

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

  16. algesten commented on Dec 11, 2016

    @algesten

    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
  17. glaebhoerl commented on Dec 11, 2016

    @glaebhoerl
    Contributor

    @algesten See also #373, #1303

  18. BattyBoopers commented on Jan 25, 2017

    @BattyBoopers

    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");
        }
  19. mdinger commented on Jan 26, 2017

    @mdinger
    ContributorAuthor

    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.

  20. comex commented on Mar 10, 2017

    @comex

    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.

  21. petrochenkov commented on Mar 10, 2017

    @petrochenkov
    Contributor

    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).

  22. gaurikholkar-zz commented on Aug 11, 2017

    @gaurikholkar-zz

    Hey, any updates on this?

  23. I60R commented on Nov 28, 2017

    @I60R

    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() {
            ...
        }
  24. added
    T-langRelevant to the language team, which will review and decide on the RFC.
    on Dec 24, 2017
  25. SoniEx2 commented on Feb 12, 2018

    @SoniEx2

    "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.)

  26. petrochenkov commented on Feb 21, 2018

    @petrochenkov
    Contributor

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

  27. Centril commented on Oct 7, 2018

    @Centril
    Contributor

    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-langRelevant to the language team, which will review and decide on the RFC.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions