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

Comments

Projects
None yet
@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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor

lilyball commented Mar 16, 2015

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

This comment has been minimized.

Copy link
Contributor Author

mdinger commented Mar 16, 2015

That's better than my idea.

@P1start

This comment has been minimized.

Copy link
Contributor

P1start 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

This comment has been minimized.

Copy link
Contributor

lilyball commented Mar 16, 2015

@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

This comment has been minimized.

Copy link

NeoLegends commented May 26, 2016

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

czakian commented Oct 13, 2016

I would also like this feature.

@aleics

This comment has been minimized.

Copy link

aleics commented Nov 14, 2016

this would be great!

@ajdlinux

This comment has been minimized.

Copy link

ajdlinux commented Nov 21, 2016

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

@jhaberstro

This comment has been minimized.

Copy link

jhaberstro commented Nov 24, 2016

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

@algesten

This comment has been minimized.

Copy link

algesten commented Dec 11, 2016

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

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Dec 11, 2016

@algesten See also #373, #1303

@0x53ee71ebe11e

This comment has been minimized.

Copy link

0x53ee71ebe11e commented Jan 25, 2017

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

gaurikholkar commented Aug 11, 2017

Hey, any updates on this?

@I60R

This comment has been minimized.

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() {
        ...
    }
@SoniEx2

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor

petrochenkov commented Feb 21, 2018

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

@Centril

This comment has been minimized.

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
You can’t perform that action at this time.