Repository navigation
Support && in if let expressions #929
Description
Activity
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.
Reacted by Clément Renault, Hans Brende, Eirik A and shridhar hegde@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
enumvariants but everyelsestatement is essentially the same (I'm not sure they could collapse to a singleelsecondition 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!") } }
Reacted by NicolasRoelandtAlso,
|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!") } }
I discovered it's possible to implement
derefwhich 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 surederefcould 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), } }
if letcame 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 awhereclause, 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.Reacted by Moritz Gunz, Arne Bahlo, Alex, Marcel Müller, ZW, Alexis Agahi, Seth Lopez, Andrew Donnellan, delacian, Andres Rios and 121 moreThat's better than my idea.
Reacted by Gustaf Borgström, huntj88, Max Nanasy, Jeremy Cochoy, YG, stackotter, Sasha Kondrashov, Marvin and DavindoIndoA more general feature that would allow this would be making
PATTERN if let PATTERN = EXPRa pattern itself, sort of like a guard but forif let. That could even be used outsideif 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 letguards for flexible patterns everywhere, and the comma syntax as syntactic sugar for something like the above.Reacted by Gábor Lehel, Addison Bean, I60R, Vigilans, Michael Pfaff, Solomon Ucko, Eira Fransham and Vivian McKnight@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 = awith this proposed if the value is a by-move.That said, I'm inclined to say that maybe we should allow
if letin 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 enhancedif letsyntax into pattern guards like you're suggesting.Reacted by Zoey, Smoothstep and Vivian McKnightI 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.
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#L122Reacted by Solomon Ucko and aur3l14noAnother option would be making
let PATTERN = EXPRESSIONan 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
xtrueifoptionwasSome,falseotherwise, and e.g.let x = let Some(b) = option && !b;would make
xtrueifoptionwasSome(false), orfalseotherwise. You get the idea.For
||, you'd have to bind the same name on both sides, just like with|inmatch, 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.Reacted by Collin J. Sutton, Kornel, Benedikt Radtke, Benjamin Levy, InfiniteCoder, Rasmus Söderhielm and Jacob SchneiderReacted by Marcel Müller, vaartis, Techcable, Hector Maddock-Greene, Felix S., Sander Maijers, Tim Diekmann and AdamI would also like this feature.
this would be great!
Just ran into this today. I really like the Swift syntax.
Reacted by Addison Bean, Hector Maddock-Greene, Sebastiaan Vermeulen, Benjamin Levy, Anton Shkurenko and Ryang SohnThis is my first day using Rust and I already ran into the need for this feature.. :)
👍Reacted by Alexander Menshikov, Sam El-Borai, Ranveer Aggarwal, Shiv Jha-Mathur, Melanie, Addison Bean, Lilith River, Hector Maddock-Greene, I60R, min and 13 moreAnother 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
Reacted by Ian Wagner, Drew Youngwerth and Solomon UckoReacted by Kornel, Sander Maijers, Oleg Nosov, Damian Carrillo, Mykhailo Krainik, Maximilian Thorn and Hazer HazerThe 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"); }
Reacted by Matthew Piziak, Stuart Small, Alex, Bernardo Meurer, Vivek Ghaisas, Alex Malikov, Kornel, Gauri Kholkar, Nathan Flurry, Hector Maddock-Greene and 28 moreReacted by Michał Nazarewicz, Pavel Chuprikov, Sun Lei, Shiv Jha-Mathur, Andrej Mihajlov, I60R, Daniel Kerbel, Eliza Weisman, Swoorup Joshi, Lukasz Guminski and 10 moreReacted by Techcable, Benedikt Radtke, Tim Diekmann, Michael-Dutchband, Benjamin Levy, Heechul Ryu and Matjaz HirschmanThis 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.
Reacted by BattyBoopers, Shotaro Yamada, delacian, Hans Brende, Daniel Kerbel, Ellen Emilia Anna Zscheile, Michael Pfaff, YG and gdor-11It 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 = bbecame an expression (returning bool) in Rust, combined with the existing variable initializedness checks, I think both&&andif !let(aka guard let) would mostly just work. One giant caveat is scoping: for normalif lets you want the binding to be visible only inside the 'then' block, while forif !letthe binding needs to last for the block containing theif. I don't know if there's a good way to solve this.Reacted by Vadim Petrochenkov, Richard Janis Goldschmidt, jD91mZM2, Boscop, Hans Brende, Eddy Cizeron and Gustaf BorgströmIt 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 letis 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).Reacted by Pyry Kontio, Remi Rampin, Ronny Herzog, Matt Johnston, Richard Janis Goldschmidt, mark, Kornel, pandaman, Tim Ryan, Sun Lei and 45 moreReacted by Alex Kladov and BoscopReacted by Alex Kladov and BoscopReacted by Hans Brende, Jokler, Alex Kladov, Jon Gjengset, Boscop, Anders Musikka and Sergio García PradoHey, any updates on this?
Reacted by Boscop, Tim Nielens, Ashish Myles, djrenren, Melanie, Maciej Goszczycki, Hans Brende, Kanedias, Sebastian Malton, Sander Maijers and 6 more- added a commit that references this issue
on Oct 5, 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
ifexpressions: 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 ofifdon't have negation and is "assymetric" with boolean version
For the first:
Compatibility between two versions ofifs 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
matchexpression.
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/destructuringifs can be separated with commaif 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 againstlet $PATTERN$ = $EXPRESSION$to returnboolbecause it could be possible to writelet 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 beforelet. Then it will be to possible to use it on multipleif letpatterns.
Also compiler should enforce_on negated type to not bring nonsense variables into scope:if let !Err(_) = try_prepare(), Ok(something) = try_get() { ... }
Reacted by Matthew James Briggs and Heechul Ryu- addedT-langRelevant to the language team, which will review and decide on the RFC.Relevant to the language team, which will review and decide on the RFC.
on Dec 24, 2017 "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.)
Notifying on this thread as well: proof-of-concept implementation of #929 (comment) is available, see #2260 (comment) for detailed description.
Closing in favor of accepted RFC #2497.
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
&&inif letexpressions. 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.Technically,
&&is typically used withboolso&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
elseto examples and an example of a deeper nesting example.