From 3405606a7c41a6d7dad4d4b642d17cb5cf518549 Mon Sep 17 00:00:00 2001 From: hamidreza kalbasi Date: Sun, 8 Aug 2021 15:14:22 +0430 Subject: [PATCH 01/19] init let expression rfc --- text/0000-let-expression.md | 626 ++++++++++++++++++++++++++++++++++++ 1 file changed, 626 insertions(+) create mode 100644 text/0000-let-expression.md diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md new file mode 100644 index 00000000000..346ef7449b1 --- /dev/null +++ b/text/0000-let-expression.md @@ -0,0 +1,626 @@ +- Feature Name: let_expression +- Start Date: 2021-08-08 +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + +# Summary +[summary]: #summary + +Convert `let = ` from a statement to an expression of type `bool`. Make +the language more consistent and remove corners by generalizing, plus adding +a bunch of magic constructs that are useful. After this RFC, you'll be able to write, among many other things: + +```rust +// generalized matches! macro: +assert!((let Some(x) = y) && x > 2); + +// generalized let-else construct +(let Some(a) = b) && (let Some(c) = f(&a)) || { + return Err("failed"); +}; +println!("{}, {}", a, c); + +// generalized assignment with default +(let Some(x) = y) || (let Foo(x) = bar) || (let x = default); +println!("{}", x); +``` + +# Motivation +[motivation]: #motivation + +The main motivation for this RFC is improving consistency and ergonomics of language. + +## Consistency + +Currently we have `if`, `if let` and `if let && let` and we teach them as three different +constructs (plus `let else` in future). But if we make `let` a `bool` expression, all will become the same and it would be +easier for new learners to get it. After this RFC you get only an unused parenthesis warning +for `if (let Some(x) = y) { }`, not a hard error. And it will have the same behavior with +classic `if let`. This is actually [a mistake by a new learner](https://github.com/rust-lang/rust/issues/82827) +that show us new learners expect it. + +This situation is worse with `if let chain` that mix let expressions with `&&` and +other bools. In fact the compiler will understand it via interpreting let as an +expression, so why we force humans to understand it another way? + +This proposal is also in-line with "everything is an expression" that we have +in rust. + +## Ergonomics + +It also available many super powers for us that can +help decreasing rightward drift without adding to implementation and understanding complexity, and +actually decreasing it by removing `let-else` and preventing from future similar constructs. + +### Compare to `let-else` + +```rust +// simple let else +let Some(x) = y else { + return Err("fail"); +}; + +// with let expression +(let Some(x) = y) || { + return Err("fail"); +}; + +// or even better +(let Some(x) = y) || return Err("fail"); + +// let else else future possiblity +let Some(x) = a else b else c else { return; }; + +// with let expression +(let Some(x) = a) || (let Some(x) = b) || (let Foo(x) = bar) || { return; }; +``` + +### New constructs + +```rust +// reuse if let body +if let Some(x) = a || let Some(x) = b || Ok(x) = c { + // body with many lines of code +} else { + // else with many lines of code +} + +// today alternative without code duplication: +if let ((Some(x), _, _) | (_, Some(x), _) | (_, _, Ok(x)) = (a, b, c) { + +// assignment with default +(let Foo(x) = a) || (let Bar(x) = b) || (let x = default); + +// today alternative: +let x = if let Foo(x) = a { x } else if let Bar(x) = b { x } else { default }; + +// simple let expression +assert!((let Some(x) = a) && (let Some(y) = b(x)) && x == y); + +// today alternative with if let chains: +assert!(matches!(a, Some(x) if let Some(y) = b(x) && x == y)); +assert!(if let Some(x) = a && let Some(y) = b(x) && x == y { true } else { false }); +``` + +## Why now? +This RFC exists thanks to people who choose `if let` for syntax we know today. If those +people had chosen anything else, for example `iflet`, `if match`, `let if` or another `keyword`, this +RFC would have been killed in the womb (Or it came in a completely different form and with +other capabilities) But they randomly chosen `if let` and we are here. Similarly people +who choose `&&` for `if let chain` could kill this. They didn't choose `&&` as randomly as +choosing `if let` and had let expression in their mind, but they had other options like `,` on +the table. + +But luck is not always with us. We can't expect each new RFC to randomly add another +piece of the let expression puzzle to the language. For example `matches!` and `let-else` are +against this and `let-else` is the possible killer for this RFC. Fortunately, people have +felt that `let-else` is not compatible with `if let chain`, and this is one of the unresolved questions. The +answer to this question is: they are not compatible! This RFC with less addition to language grammar and +more expressive power is superior. + +So goal of proposing this now is to prevent `let-else` and future similar RFCs to be stabilized. Originally +authors of `if let chain` had an incremental plan toward let expression. +But the implementation of the first part took a long time (which is not over yet) so we are here. If authors +of `if let chain` had submitted a complete proposal from the beginning and it had been accepted, we +would not have seen things like `let-else` at all. + +Even if it doesn't fit in this year road-map, we should decide if we want it or not today. Even today +is late! + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +This section examines the features proposed by this RFC. + +## `let` as a bool expression + +The `let = ` returns a bool expression that returns `true` when `` matches the `` +and otherwise will be evaluated to `false`. For example we have this: + +```rust +let bar = Some(4); +assert!(let Some(x) = bar); + +let foo = 'f'; +assert!(let 'A'..='Z' | 'a'..='z' = foo); +``` + +## Binding statements + +Every `let` expression have some (maybe zero) free variable in it's pattern that +we call them PBs (positive bindings) of a let expression. If a bool expression +comes with a `;` (as a statement) and compiler can prove it is always `true` (for simple +let expressions it means pattern is irrefutable) it will bind all PBs to the local scope +after `;` and init them with result of pattern matching. So we have this: +```rust +let a = 2; +let Point { x, y, z } = p; +// we have a, x, y, z here +``` + +## Combining with `||` +If we combine two let expressions via `||`, their PBs should be equal, otherwise +we will get a compile error. PBs of result expression is equal to PB of it's operands. So +from previous part we have: + +```rust +(let Some(x) = foo) || (let x = default); +// we have x here +``` +How it will be run? We will reach first line, then: +* If foo matches Some(x), we fill `x` based of foo, `(let Some(x) = foo)` will be evaluated to true, and short circuit the `||` so go to next line. +* Otherwise we will go to next operand, assign default to x, evaluate `(let x = default)` to true and go to next line. + +Why their PBs should be equal? Because from knowing that the expression is true, we +know one side of the `||` is true, but we don't know which side is true. If their +PBs is equal (name-vise and type-vise) we can sure that they can be filled in +run-time, either from first operand or second operand. So they must be equal. + +This limit isn't new. We already have it in `|` pattern bindings. Today, `let (Some(y) | None) = x;` doesn't compile +with error `variable y is not bound in all patterns`. And in let expression equivalent form `(let Some(y) = x) || (let None = x);` we +will get a similar error `variable y is not bound in all cases`. + +In addition to `true`, binding statements are allowed to diverge (have type of `!`) so +we can write this: + +```rust +(let Some(x) = foo) || panic!("foo is none"); +println("{}", x); +``` + +But what about rule of equal PBs? What is PB set of `panic!("foo is none");`? As `!` can cast +to all types, their PBs can cast to any set of PB and wouldn't make an error. This make +sense because we don't care about after a return or a panic. + +## Combining with `&&` +If we combine two let expressions via `&&`, PBs of whole expression would be the +merged set of both PBs. So we will have: +``` +(let a = 2) && (let Point { x, y, z } = p); +// we have a, x, y, z here +``` +These are useless alone (equal to separating with `;`) but can become useful with `||`: +``` +(let Some(x) = foo) && (let Some(y) = bar(x)) || (let x = default) && (let y = default2(x)); +``` +Also, in `EXP1 && EXP2` you can use and shadow PBs of `EXP1` inside `EXP2`. This +is because if we are in `EXP2` we can be sure that `EXP1` was true because +otherwise `&&` would be short circuited and `EXP2` won't run. Example: + +```rust +let foo = Some(2); +((let Some(x) = foo) || panic!("paniiiiiiic")) && (let y = x + 3); +println!("{}, {}", x, y); // 2, 5 +let a = Some(y); +println!("{}", (let Some(b) = a) && b > 3); // true +``` + +And you can mix this with normal bool expressions. They have zero PB but act like +any other bool expression. So we can have this: +```rust +is_good(x) && (let y = Some(x)) || (let y = None); +// we have y: Option here +``` + +## Combining with `!` +If we negate a bool expression, all of its PBs become NB (Negative binding) and +its NBs become PB. NBs behavior in `&&` and `||` is reversed. In `&&` they should +be equal and in `||` they will be merged. + +## Consuming bool expressions +If we consume a bool expression in anything other than bool operators (such as +function calls or assignments or match expressions) it would lose its PBs and NBs. +```rust +let bar = Some(Foo(4)); +assert!((let Some(x) = bar) && (let Foo(y) = x) && y > 2); +// no x and y here +``` + +Some more complex examples: + +```rust +let is_some = let Some(x) = opt; +``` +In this example, `is_some: bool` is binded, but `x` isn't and compiler will say it is `unused_variable`. + +Another example: +```rust +let is_foo = (let Some(x) = opt) && foo(x); +``` +And here `is_foo: bool` is binded, `x` isn't and there is no warning because `x` is used in `foo(x)`. Note that if we +remove `()` from `(let Some(x) = opt)` it will becomes `opt && foo(x)` so doesn't compile. + +## `()` vs `{}` + +Specially, `{}` expressions will consume bools and lose its PBs and NBs. This behavior is +consistent with our expectation from `{}` that have bindings only local to itself. So for example: +``` +assert!((let Some(x) = foo) && (x.is_bar()) || baz == 2); +``` +Doesn't compile because of different PBs in `||` (`baz == 2` has no PB but `(let Some(x) = foo) && (x.is_bar())` has `x`) but +``` +assert!({ (let Some(x) = foo) && (x.is_bar()) } || baz == 2); +``` +will compile, because `{}` would discard all of bindings. With `()` instead of `{}` we will get same error +of first example. + +## `if` and `while` + +From definition of `if` we know: +```rust +if b { + // if we are here b is true +} else { + // if we are here b is false +} +``` + +So compiler can (and will) allow us to access PBs inside of then block and NBs inside of else block. + +## Anything else? + +No. Hurrah, you just learned all of `if let` and `while let` and `if let chain` and (an alternative syntax for) `let-else` without +one line of sugar and `match` and inconsistency and special case. + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +Many parts of this proposal (such as grammar changes) are already implemented for +the `if-let-chains` RFC. + +Previously, `if let` and `if let chains` implementations was via desugaring to match expression. This +is useful because it doesn't create new rules for borrow checker and scoping. We can do the same +with this proposal and do just some desugaring, as explained below. In addition to desugaring, we +need to implement PB and NB concept in the compiler as explained in guide-level explanation. Also +it has some problems that we explain later. + +## Desugar rules +`&&` in top level of if scrutinee: +```rust +if a && b { + EXPR_IF +} else { + EXPR_ELSE +} +``` +would become: +```rust +if a { + if b { + EXPR_IF + } else { + EXPR_ELSE + } +} else { + EXPR_ELSE +} +``` +and `||` become: +```rust +if a { + EXPR_IF +} else { + if b { + EXPR_IF + } else { + EXPR_ELSE + } +} +``` +and `!`: +```rust +if !a { + EXPR_IF +} else { + EXPR_ELSE +} +``` +become: +```rust +if a { + EXPR_ELSE +} else { + EXPR_IF +} +``` +We will follow this desugar rules until we reach atomic `if let` and `if bool` and +desugar them to `match` expressions as we do it today. + +While has a syntax sugar from `if let chain` proposal which desugars: +``` +while condition { + EXPR_WHILE +} +``` +into: +``` +loop { + if condition { + { EXPR_WHILE } + continue; + } + break; +} +``` + + +Consumed let statements in function calls or other places will change from `b` into: +```rust +if b { true } else { false } +``` + +Binding statements that contains just a simple `let` work today. +For desugaring complex binding statements we need to compute PBs of the binding statement, then +we can convert it to: +```rust +let (PB1, PB2, PB3, ...) = if BINDING_STMT { + (PB1, PB2, PB3, ...) +} else { + unreachable!() + //compiler should prove this or return a compile error. +} +``` + +## Prove binding statement is always `true` +We can outsource this to the smartness of compiler. If a human use a complex binding statement +that believes it is ok (and it isn't a wrong assumption) there is no point in rejecting that. This +is contrary to binding rules in which we use strong static rules. If we rely on smartness of compiler, +it can allow us: +```rust +if !let Some(x) = foo { + return; +} +// compiler can figure out that accessing x here is ok +// but we don't allow this because it is unclear for humans +// and can create problem in combination with shadowing +// so changing it can be breaking +``` +But this is harmful and we don't allow this. But for checking binding statements even surprising ones like: +```rust +if let None = foo { + return; +} +let Some(x) = foo; +// use x here +``` +has no harm. People can try if compiler is smart enough to understand their code, and if it isn't +and they are sure that their binding statement is always true, they can add a `|| unreachable!()` at the end +manually. + +For start, we can allow trivial cases, e.g. `let = expr`, `divergent`, `true && true`, +`x || true`, ... . In next steps things like `(let Foo(x) = y) || (let Bar(x) = y)` can be allowed. And +allowing something like above example seems infeasible in near future. + +## Rules of PB and NB +What would be happen if we don't check those rules? For example look at desugar of `||`: +```rust +if a { + EXPR_IF +} else { + if b { + EXPR_IF + } else { + EXPR_ELSE + } +} +``` +When this will compiles? It will compiles when bindings of `EXPR_if` is subset of PBs of both `a` and `b` so +a generalized and natural rule for PBs and NBs for `||` would be union of NBs and intersection of PBs. This +doesn't need any more check for PBs and NBs. But +this can confuse humans, especially in combination with shadowing: +```rust +let x = 2; +if (let Some(x) = foo) || is_happy(bar) { + // x is 2, even if foo is Some(_) +} +``` +If people really need this behavior and doesn't made it by mistake, they can do: +```rust +let x = 2; +if { let Some(x) = foo } || is_happy(bar) { + // x is 2, even if foo is Some(_) +} +``` +which explicitly shows that x is local to that block. + +This extra limit is also consistent with other parts of the language. We could take a similar approach +in `|` pattern and silently don't bind `y` in a pattern like `Some(y) | None` so there wouldn't be +an error until `y` used. But people decided against this (with good reason) and this RFC follow them +in this decision. + +## Divergent case + +It should be noted that divergent expressions are specially handled. If they happen in top-level +of if scrutinee, body of if is unreachable and we discard it. For example this: +```rust +(let Some(x) = foo) || panic!("foo is none"); +println!("{}", x); +``` +would become normally to: +```rust +let x = if let Some(x) = foo { + x +} else { + if panic!("foo is none") { + x + } else { + provably_unreachable!(); + } +}; +println!("{}", x); +``` +which doesn't compile (because second x isn't declared). But desugar procedure can remove second if safely: +```rust +let x = if let Some(x) = foo { + x +} else { + panic!("foo is none") +}; +println!("{}", x); +``` +If we don't do this, PB set of divergent expressions would become empty set like other bools. But +it limit use-cases of let expression. So we handle divergent case in this way. + +## Code duplication +As you see, code is duplicated in desugaring, and this can be exponential. This is unacceptable +in compiler. `if let chain` RFC prevent this problem with desugaring this +```rust +if let PAT_1 = EXPR_1 + && let PAT_2 = EXPR_2 + && EXPR_3 + ... + && let PAT_N = EXPR_N +{ + EXPR_IF +} else { + EXPR_ELSE +} +``` + +into: + +```rust +'FRESH_LABEL: { + if let PAT_1 = EXPR_1 { + if let PAT_2 = EXPR_2 { + if EXPR_3 { + ... + if let PAT_N = EXPR_N { + break 'FRESH_LABEL { EXPR_IF } + } + } + } + } + { EXPR_ELSE } +} +``` + +We can't use this as-is. Because it lose else part so it can't apply recursively. But we maybe +able to do something like this (for example for `||`): + +```rust +if a { + 'here: EXPR_IF +} else { + if b { + goto 'here; + } else { + EXPR_ELSE + } +} +``` + +This is not valid rust syntax so we can't call it desugaring. but if we check that context +in those positions are equal (rules of PBs and NBs) we can do that jump safely. + +## Implementing without sugar +Implementors are free to implement it another way, for example implement let expressions directly. +They should take desugaring behavior (the one with code duplicating) as a reference and +implement the same behavior in a desired way. + +# Drawbacks +[drawbacks]: #drawbacks + +This RFC is big and the language specification +is possibly made more complex by it. While this complexity will be used by some +and therefore, the RFC argues, motivates the added complexity, it will not be +used all users of the language. However, +by unifying constructs in the language conceptually, +we may also say that complexity is *reduced*. Specially when we think about +macros and RFCs that this RFC will prevent. Macros and special constructs are +simple patterns with this RFC. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +## The `is` operator +Some people argue that `let` doesn't read well as an expression. So we introduce +an operator ` is ` equal to `let = ` expression that explained +in this RFC. This may read better in context of `if` but it has some problems: +* It is a new construct +* It is an infix operator, those can become hard in parsing +* It will duplicate some part of languages. After implementing it, we will + deprecate `if let`? It make a huge gap between old and new rust code. +* If we put it in this RFC as-is, we should accept `2 is x;` as a declaration + for `x` which isn't familiar for programmers and also duplicates let and has problem above. + +## Macros +We can detect popular patterns that this RFC makes possible and create special macros +for them. `matches!`, `if_chain!` and `guard!` macro are today examples of this and +we can add more later. + +But macros are complex and everyone should learn every of them separately. A consistent +language feature that make couple of macros unnecessary is better. Also, `let-else` and +similar proposals shows that macros aren't enough. + +## A subset of this RFC +Not all things introduced here are useful and some of them are because consistency and completeness. We can make +some subsets of this RFC hard errors and make them future possiblity. Subsets that are candidate of removing +are: +* The `!` operator: +we can remove `!` operator so there would be no NB and whole thing becomes simpler to understand. But there may +be some patterns that `!` make sense for them and we don't know today. Also we can lint and warn against unnecessary +usages of `!` operator. But making this hard error can become surprising because we allow negation of simple bools. +* Consuming let expressions outside of if and binding statements: +Some people argue that let expressions in arbitary places can be confusing because the scope of bindings in not clear, +and they are useless for common cases (simple matching) in presence of `matches!` macro. +"Consuming expressions outside of if scrutinee and toplevel of block will discard bindings" is a easy to +remember rule, but somehow it isn't visually clear. By rejecting those with a hard error, this concern will be +solved but we will lose the mental model of "every let expression is a simple bool expression". We can mandate +a `{}` block around consumed let expressions to make scope visually clear but this is a surprising restriction that +exists currently nowhere in the language. As another claim against this alternative, situations that compiler +doesn't catch with a undefined variable error or a unused variable warning are extremely rare, so compiler will +teach developers binding rules, which are easy to learn. + +# Prior art +[prior-art]: #prior-art + +There is a great discussion around this topic in this RFCs and their comments: +* [RFC 2497 (if let chain)](https://github.com/rust-lang/rfcs/blob/master/text/2497-if-let-chains.md) and [comments](https://github.com/rust-lang/rfcs/blob/master/text/2497-if-let-chains.md) +* [Comments of let else 2015 RFC](https://github.com/rust-lang/rfcs/pull/1303) (it was at first `if !let`) [and related issue](https://github.com/rust-lang/rfcs/issues/2616) +* [RFC 160 (if let) and comments](https://github.com/rust-lang/rfcs/pull/160) + +In other languages, there are `is operator` somehow similar to let expression proposed here: +* [Kotlin](https://kotlinlang.org/docs/typecasts.html) +* [C#](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/is) + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +To be determined. + +# Future possibilities +[future-possibilities]: #future-possibilities + +## Changing precedence of `&&` and `||` +Today, as you see in examples, `()` around `let` is mandatory because assignment has lower precedence +than `||` and `&&`. Assigning bools to variables is rare so asking them to put a parenthesis around them +isn't so bad. This is already changed in if scrutinee in `if let chain` RFC as edition `2018`. A future +RFC in edition `2024` or `2027` can change it in other places and make language consistent and remove +unnecessary parenthesis from a then-popular pattern. + +## Convert assignment to a bool expression +In [RFC 2909](https://github.com/rust-lang/rfcs/blob/master/text/2909-destructuring-assignment.md) we +allow destructing on assignments. A future RFC can make them a bool expression which returns true if +pattern matched. + From c2bdecdf938151c27abc0f8cc396141215963fba Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Mon, 9 Aug 2021 15:05:26 +0430 Subject: [PATCH 02/19] change first paragraph of #why now? --- text/0000-let-expression.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index 346ef7449b1..718fa4ac6f2 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -103,13 +103,15 @@ assert!(if let Some(x) = a && let Some(y) = b(x) && x == y { true } else { false ``` ## Why now? -This RFC exists thanks to people who choose `if let` for syntax we know today. If those -people had chosen anything else, for example `iflet`, `if match`, `let if` or another `keyword`, this -RFC would have been killed in the womb (Or it came in a completely different form and with -other capabilities) But they randomly chosen `if let` and we are here. Similarly people +This RFC exists thanks to people who choose `if let` for syntax we know today. +That syntax wasn't the only choice and there was other options like `iflet`, `if match`, `let if`, `if is` or another `keyword`. +If they chose one of the alternatives, no one would have even imagined the let expressions in form of this RFC +and this RFC either did not come into being at all or came in a completely different +form and with different capabilities. +But they chosen `if let` among other options (with good reasons) and we are here. Similarly people who choose `&&` for `if let chain` could kill this. They didn't choose `&&` as randomly as choosing `if let` and had let expression in their mind, but they had other options like `,` on -the table. +the table, which wasn't compatible with let expressions. But luck is not always with us. We can't expect each new RFC to randomly add another piece of the let expression puzzle to the language. For example `matches!` and `let-else` are From c86f4c2126118f7528ad038ca07eb0c1ae161ee2 Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Wed, 11 Aug 2021 01:25:01 +0430 Subject: [PATCH 03/19] Update 0000-let-expression.md --- text/0000-let-expression.md | 71 +++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index 718fa4ac6f2..55a5e5d399d 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -543,6 +543,8 @@ implement the same behavior in a desired way. # Drawbacks [drawbacks]: #drawbacks +## Big change in language + This RFC is big and the language specification is possibly made more complex by it. While this complexity will be used by some and therefore, the RFC argues, motivates the added complexity, it will not be @@ -552,6 +554,59 @@ we may also say that complexity is *reduced*. Specially when we think about macros and RFCs that this RFC will prevent. Macros and special constructs are simple patterns with this RFC. +## Hard to read let expressions + +Aggressive use of let expressions can lead to complex and hard to read results: +```rust +( + (( + (let Some(x) = a) + && (let Some(y) = x.transform()) + ) || { panic!("failed to get y") }) + && ( + (let Some(a) = y.transform1()) + || (let Ok(a) = y.transform2()) + || (let Some(a) = if let either = y.transform3() && let Either::Left(left) = either { + Some(transform_left(left)) + } else { + None + }) + ) +) +|| panic("fun just ended!"); +``` +it can be written on one line, but hopefully rustfmt will prevent that. Also rules of bindings will prevent +people to write arbitary let expressions. For example: +```rust +(let Some(a) = y.transform1()) +|| ((let result = y.transform2()) && ((let Ok(a) = result) || { return result; })); +println!("{}", a); +``` +won't compile because PB set of `(let Some(a) = y.transform1())` doesn't contains `result`. + +This problem is not limited to let expressions and all powerful structures have it. In +particular, regular expressions correspond to patterns: `let a = b && let c = d` is roughly +equivalent to `let (a, c) = (b, d)` and `let a = b || let c = d` is roughly equivalent to `let ((a, _) | (_, c)) = (b, d)` so +every complex let expression has a dual complex expression with patterns (with different behaviour and capabilities), example for above one: +```rust +let (Some(x), Some(y), (Some(a), _, _) | (_, Ok(a), _) | (_, _, Some(a))) = ( + a, + x.transform(), + ( + y.transform1(), + y.transform2(), + if let (either, Either::Left(left)) = (y.transform3(), either) { + Some(transform_left(left)) + } else { + None + }, + ), +); +``` +This doesn't have divergents and doesn't behave as intended, but shows that same complexity is possible in patterns, +And since this complexity in the patterns did not cause a serious problem, we can hope that +it does not cause a problem in let expressions either. + # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives @@ -575,6 +630,22 @@ But macros are complex and everyone should learn every of them separately. A con language feature that make couple of macros unnecessary is better. Also, `let-else` and similar proposals shows that macros aren't enough. +## let-else RFC +A large part of the this RFC interferes with the let-else RFC, and in fact one of the purposes of this is +to replace the let-else. Although let-else do its job well, the expressive power of let expressions is much greater +and they are more consistent with the rest of the language (especially if-let-chain). If we need language +changes for this feature, why make a change just for this particular application? With let expression, this +RFC and similar RFCs in the future won't be happen and their task will be taken with this consistent syntax. + +Some people argue that `else` is a better choice and `||` doesn't read very well. But in fact using +short circuit operators in this way is a wellknown pattern in general, and it is popular in bash scripting. In +standard ML, short circuit operators are called `OrElse` and `AndAlso` which shows that this similarity is known +and `else` in let-else is more like `OrElse` rather than `else` in if-else, so `||` is a good choice. + +Another benefit is that grammar changes of this RFC are done in if-let-chain and grammar rules of +`||` and `&&` is simple and wellknown but there are some concerns and special cases +about let-else grammar when mixing it with if-else and if-let. + ## A subset of this RFC Not all things introduced here are useful and some of them are because consistency and completeness. We can make some subsets of this RFC hard errors and make them future possiblity. Subsets that are candidate of removing From 30e09b4ea9f6c8ee9a11763793eb3a4bf6b9da3b Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Wed, 11 Aug 2021 14:44:09 +0430 Subject: [PATCH 04/19] Prepare to remove not operator --- text/0000-let-expression.md | 69 +++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index 55a5e5d399d..c55abd60bef 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -150,9 +150,9 @@ assert!(let 'A'..='Z' | 'a'..='z' = foo); ## Binding statements Every `let` expression have some (maybe zero) free variable in it's pattern that -we call them PBs (positive bindings) of a let expression. If a bool expression +we call them bindings of a let expression. If a bool expression comes with a `;` (as a statement) and compiler can prove it is always `true` (for simple -let expressions it means pattern is irrefutable) it will bind all PBs to the local scope +let expressions it means pattern is irrefutable) it will bind all bindings to the local scope after `;` and init them with result of pattern matching. So we have this: ```rust let a = 2; @@ -161,8 +161,8 @@ let Point { x, y, z } = p; ``` ## Combining with `||` -If we combine two let expressions via `||`, their PBs should be equal, otherwise -we will get a compile error. PBs of result expression is equal to PB of it's operands. So +If we combine two let expressions via `||`, their bindings should be equal, otherwise +we will get a compile error. Bindings of result expression is equal to bindings of it's operands. So from previous part we have: ```rust @@ -173,9 +173,9 @@ How it will be run? We will reach first line, then: * If foo matches Some(x), we fill `x` based of foo, `(let Some(x) = foo)` will be evaluated to true, and short circuit the `||` so go to next line. * Otherwise we will go to next operand, assign default to x, evaluate `(let x = default)` to true and go to next line. -Why their PBs should be equal? Because from knowing that the expression is true, we +Why their bindings should be equal? Because from knowing that the expression is true, we know one side of the `||` is true, but we don't know which side is true. If their -PBs is equal (name-vise and type-vise) we can sure that they can be filled in +bindings is equal (name-vise and type-vise) we can sure that they can be filled in run-time, either from first operand or second operand. So they must be equal. This limit isn't new. We already have it in `|` pattern bindings. Today, `let (Some(y) | None) = x;` doesn't compile @@ -190,13 +190,13 @@ we can write this: println("{}", x); ``` -But what about rule of equal PBs? What is PB set of `panic!("foo is none");`? As `!` can cast -to all types, their PBs can cast to any set of PB and wouldn't make an error. This make +But what about rule of equal bindings? What is binding set of `panic!("foo is none");`? As `!` can cast +to all types, their bindings can cast to any set of bindings and wouldn't make an error. This make sense because we don't care about after a return or a panic. ## Combining with `&&` -If we combine two let expressions via `&&`, PBs of whole expression would be the -merged set of both PBs. So we will have: +If we combine two let expressions via `&&`, bindings of whole expression would be the +merged set of both bindings. So we will have: ``` (let a = 2) && (let Point { x, y, z } = p); // we have a, x, y, z here @@ -205,7 +205,7 @@ These are useless alone (equal to separating with `;`) but can become useful wit ``` (let Some(x) = foo) && (let Some(y) = bar(x)) || (let x = default) && (let y = default2(x)); ``` -Also, in `EXP1 && EXP2` you can use and shadow PBs of `EXP1` inside `EXP2`. This +Also, in `EXP1 && EXP2` you can use and shadow bindings of `EXP1` inside `EXP2`. This is because if we are in `EXP2` we can be sure that `EXP1` was true because otherwise `&&` would be short circuited and `EXP2` won't run. Example: @@ -217,7 +217,7 @@ let a = Some(y); println!("{}", (let Some(b) = a) && b > 3); // true ``` -And you can mix this with normal bool expressions. They have zero PB but act like +And you can mix this with normal bool expressions. They have no binding but act like any other bool expression. So we can have this: ```rust is_good(x) && (let y = Some(x)) || (let y = None); @@ -225,13 +225,13 @@ is_good(x) && (let y = Some(x)) || (let y = None); ``` ## Combining with `!` -If we negate a bool expression, all of its PBs become NB (Negative binding) and +If we negate a bool expression, all of its normal bindings (which we now call positive binding or PB) become NB (Negative binding) and its NBs become PB. NBs behavior in `&&` and `||` is reversed. In `&&` they should be equal and in `||` they will be merged. ## Consuming bool expressions If we consume a bool expression in anything other than bool operators (such as -function calls or assignments or match expressions) it would lose its PBs and NBs. +function calls or assignments or match expressions) it would lose its bindings. ```rust let bar = Some(Foo(4)); assert!((let Some(x) = bar) && (let Foo(y) = x) && y > 2); @@ -243,24 +243,24 @@ Some more complex examples: ```rust let is_some = let Some(x) = opt; ``` -In this example, `is_some: bool` is binded, but `x` isn't and compiler will say it is `unused_variable`. +In this example, `is_some: bool` is bound, but `x` isn't and compiler will say it is `unused_variable`. Another example: ```rust let is_foo = (let Some(x) = opt) && foo(x); ``` -And here `is_foo: bool` is binded, `x` isn't and there is no warning because `x` is used in `foo(x)`. Note that if we +And here `is_foo: bool` is bound, `x` isn't and there is no warning because `x` is used in `foo(x)`. Note that if we remove `()` from `(let Some(x) = opt)` it will becomes `opt && foo(x)` so doesn't compile. ## `()` vs `{}` -Specially, `{}` expressions will consume bools and lose its PBs and NBs. This behavior is +Specially, `{}` expressions will consume bools and lose its bindings. This behavior is consistent with our expectation from `{}` that have bindings only local to itself. So for example: -``` +```rust assert!((let Some(x) = foo) && (x.is_bar()) || baz == 2); ``` -Doesn't compile because of different PBs in `||` (`baz == 2` has no PB but `(let Some(x) = foo) && (x.is_bar())` has `x`) but -``` +Doesn't compile because of different bindings in `||` (`baz == 2` has no binding but `(let Some(x) = foo) && (x.is_bar())` has `x`) but +```rust assert!({ (let Some(x) = foo) && (x.is_bar()) } || baz == 2); ``` will compile, because `{}` would discard all of bindings. With `()` instead of `{}` we will get same error @@ -277,7 +277,7 @@ if b { } ``` -So compiler can (and will) allow us to access PBs inside of then block and NBs inside of else block. +So compiler can (and will) allow us to access bindings inside of then block of if, or body block of while. ## Anything else? @@ -293,7 +293,7 @@ the `if-let-chains` RFC. Previously, `if let` and `if let chains` implementations was via desugaring to match expression. This is useful because it doesn't create new rules for borrow checker and scoping. We can do the same with this proposal and do just some desugaring, as explained below. In addition to desugaring, we -need to implement PB and NB concept in the compiler as explained in guide-level explanation. Also +need to implement rules of bindings in the compiler as explained in guide-level explanation. Also it has some problems that we explain later. ## Desugar rules @@ -372,11 +372,11 @@ if b { true } else { false } ``` Binding statements that contains just a simple `let` work today. -For desugaring complex binding statements we need to compute PBs of the binding statement, then +For desugaring complex binding statements we need to compute bindings of the statement, then we can convert it to: ```rust -let (PB1, PB2, PB3, ...) = if BINDING_STMT { - (PB1, PB2, PB3, ...) +let (B1, B2, B3, ...) = if BINDING_STMT { + (B1, B2, B3, ...) } else { unreachable!() //compiler should prove this or return a compile error. @@ -413,7 +413,7 @@ For start, we can allow trivial cases, e.g. `let = expr`, `diverge `x || true`, ... . In next steps things like `(let Foo(x) = y) || (let Bar(x) = y)` can be allowed. And allowing something like above example seems infeasible in near future. -## Rules of PB and NB +## Rules of bindings What would be happen if we don't check those rules? For example look at desugar of `||`: ```rust if a { @@ -426,9 +426,9 @@ if a { } } ``` -When this will compiles? It will compiles when bindings of `EXPR_if` is subset of PBs of both `a` and `b` so -a generalized and natural rule for PBs and NBs for `||` would be union of NBs and intersection of PBs. This -doesn't need any more check for PBs and NBs. But +When this will compiles? It will compiles when bindings of `EXPR_if` is subset of bindings of both `a` and `b` so +a generalized and natural rule for bindings of `||` would be intersection of bindings in both sides. This +doesn't need any more check for bindings. But this can confuse humans, especially in combination with shadowing: ```rust let x = 2; @@ -443,7 +443,7 @@ if { let Some(x) = foo } || is_happy(bar) { // x is 2, even if foo is Some(_) } ``` -which explicitly shows that x is local to that block. +which explicitly shows that `x` is local to that block. This extra limit is also consistent with other parts of the language. We could take a similar approach in `|` pattern and silently don't bind `y` in a pattern like `Some(y) | None` so there wouldn't be @@ -480,8 +480,9 @@ let x = if let Some(x) = foo { }; println!("{}", x); ``` -If we don't do this, PB set of divergent expressions would become empty set like other bools. But -it limit use-cases of let expression. So we handle divergent case in this way. +If we don't do this, binding set of divergent expressions would become empty set like other bools. But +it limit use-cases of let expression and we need them to be able to cast to every possible +set. So we handle divergent case in this way. ## Code duplication As you see, code is duplicated in desugaring, and this can be exponential. This is unacceptable @@ -533,7 +534,7 @@ if a { ``` This is not valid rust syntax so we can't call it desugaring. but if we check that context -in those positions are equal (rules of PBs and NBs) we can do that jump safely. +in those positions are equal (rules of bindings) we can do that jump safely. ## Implementing without sugar Implementors are free to implement it another way, for example implement let expressions directly. @@ -582,7 +583,7 @@ people to write arbitary let expressions. For example: || ((let result = y.transform2()) && ((let Ok(a) = result) || { return result; })); println!("{}", a); ``` -won't compile because PB set of `(let Some(a) = y.transform1())` doesn't contains `result`. +won't compile because binding set of `(let Some(a) = y.transform1())` doesn't contain `result`. This problem is not limited to let expressions and all powerful structures have it. In particular, regular expressions correspond to patterns: `let a = b && let c = d` is roughly From 9e77c595766e9c207a08eb6cff06ea06f938417f Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Wed, 11 Aug 2021 17:45:59 +0430 Subject: [PATCH 05/19] Add future of matches section --- text/0000-let-expression.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index c55abd60bef..22cc121eefa 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -115,7 +115,8 @@ the table, which wasn't compatible with let expressions. But luck is not always with us. We can't expect each new RFC to randomly add another piece of the let expression puzzle to the language. For example `matches!` and `let-else` are -against this and `let-else` is the possible killer for this RFC. Fortunately, people have +potentially against this, `matches!` can [coexist with let expression][future-of-matches] but +let-else is not compatible with let expression and they can hardly be together in one place. Fortunately, people have felt that `let-else` is not compatible with `if let chain`, and this is one of the unresolved questions. The answer to this question is: they are not compatible! This RFC with less addition to language grammar and more expressive power is superior. @@ -389,15 +390,16 @@ that believes it is ok (and it isn't a wrong assumption) there is no point in re is contrary to binding rules in which we use strong static rules. If we rely on smartness of compiler, it can allow us: ```rust -if !let Some(x) = foo { - return; -} +if let Some(x) = foo { } else { return; } // compiler can figure out that accessing x here is ok // but we don't allow this because it is unclear for humans // and can create problem in combination with shadowing // so changing it can be breaking +println!("{}", x); // compile error! ``` -But this is harmful and we don't allow this. But for checking binding statements even surprising ones like: +This is harmful and we don't allow this. + +But for checking binding statements, even surprising ones like: ```rust if let None = foo { return; @@ -631,6 +633,16 @@ But macros are complex and everyone should learn every of them separately. A con language feature that make couple of macros unnecessary is better. Also, `let-else` and similar proposals shows that macros aren't enough. +### Future of matches +[future-of-matches]: #future-of-matches + +Note that this RFC is not intended to deprecate `matches!`. `matches!` and let expressions can co-exist +together like `match` and `if let` because each has its own application. Specially for patterns that +doesn't have bindings, matches macro is superior and even a linter can suggest changing things like +`let 'a'..'z' = foo` to `matches!(foo, 'a'..'z')`. But when there are bindings, let expressions are +better, for example `let Some(x) = foo && let Some(y) = parse(x) && is_good(y)` is more clear than +`matches!(foo, Some(x) if let Some(y) = parse(x) && is_good(y))`. + ## let-else RFC A large part of the this RFC interferes with the let-else RFC, and in fact one of the purposes of this is to replace the let-else. Although let-else do its job well, the expressive power of let expressions is much greater From d6c3ac100358484e6ee7315ec875dc9b72c02698 Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Wed, 11 Aug 2021 19:44:33 +0430 Subject: [PATCH 06/19] update hard to read let expression section --- text/0000-let-expression.md | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index 22cc121eefa..151c804a33c 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -585,29 +585,23 @@ people to write arbitary let expressions. For example: || ((let result = y.transform2()) && ((let Ok(a) = result) || { return result; })); println!("{}", a); ``` -won't compile because binding set of `(let Some(a) = y.transform1())` doesn't contain `result`. +won't compile because binding set of `(let Some(a) = y.transform1())` doesn't contain `result`. This rule also +make it possible to find which variables will bound with a quick look, that is, every binding variable that +appear in a top-level let (not let expressions inside blocks or function calls) will be in the binding set of +final expression. so first example will bound `x`, `y` and `a` and we will get it with a quick look. This problem is not limited to let expressions and all powerful structures have it. In particular, regular expressions correspond to patterns: `let a = b && let c = d` is roughly equivalent to `let (a, c) = (b, d)` and `let a = b || let c = d` is roughly equivalent to `let ((a, _) | (_, c)) = (b, d)` so -every complex let expression has a dual complex expression with patterns (with different behaviour and capabilities), example for above one: -```rust -let (Some(x), Some(y), (Some(a), _, _) | (_, Ok(a), _) | (_, _, Some(a))) = ( - a, - x.transform(), - ( - y.transform1(), - y.transform2(), - if let (either, Either::Left(left)) = (y.transform3(), either) { - Some(transform_left(left)) - } else { - None - }, - ), -); +every complex let expression has a dual complex expression with patterns (with different behaviour and capabilities), example of a complex +pattern matching: +```rust +let ((Foo(x), Some(y), (Some(z), _, _) | (_, Ok(z), _) | (_, _, Some(z))) + | (Bar(z), x @ None, (Some(y), _, _) | (_, Err(y), _) | (_, _, Some(y)))) = + (a, b, (c.transform1(), c.transform2(), c.transform3())); ``` -This doesn't have divergents and doesn't behave as intended, but shows that same complexity is possible in patterns, -And since this complexity in the patterns did not cause a serious problem, we can hope that +This shows that same complexity is possible in patterns, and in both cases the complexity can be scaled to infinity. +Since this complexity in the patterns did not cause a serious problem, we can hope that it does not cause a problem in let expressions either. # Rationale and alternatives From 8561362f56e43f1f2dd5f76184e5c726ff865c56 Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Wed, 11 Aug 2021 19:46:27 +0430 Subject: [PATCH 07/19] fix a typo --- text/0000-let-expression.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index 151c804a33c..a07a6c5bda7 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -591,7 +591,7 @@ appear in a top-level let (not let expressions inside blocks or function calls) final expression. so first example will bound `x`, `y` and `a` and we will get it with a quick look. This problem is not limited to let expressions and all powerful structures have it. In -particular, regular expressions correspond to patterns: `let a = b && let c = d` is roughly +particular, let expressions correspond to patterns: `let a = b && let c = d` is roughly equivalent to `let (a, c) = (b, d)` and `let a = b || let c = d` is roughly equivalent to `let ((a, _) | (_, c)) = (b, d)` so every complex let expression has a dual complex expression with patterns (with different behaviour and capabilities), example of a complex pattern matching: From 8c2609fbf6d8ff914f8d8ace9de2bb1e12d4da2c Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Thu, 12 Aug 2021 00:39:04 +0430 Subject: [PATCH 08/19] Add Let expression type section in drawbacks --- text/0000-let-expression.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index a07a6c5bda7..ac897d26773 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -557,6 +557,27 @@ we may also say that complexity is *reduced*. Specially when we think about macros and RFCs that this RFC will prevent. Macros and special constructs are simple patterns with this RFC. +## Let expression type isn't bool in other languages + +`let` as a bool expression can become surprising for people coming from other languages. People +see `let` equal to their variable declaring statements, and let expression with `bool` type are +very different from them. + +In C family and Java, `int x = y` is equivalent of `let` in rust which isn't +expression, but `x = y` is a expression equal to `{ x = y; x }` in rust. So, people from those +language may expect `let x = y` returns `y` instead of `true`. Using these expressions is considered +an anti-pattern and therefore the rust does not have them. Due to its similarity to let expressions, it +may be thought that let expressions are also a anti-pattern, but they are different conceptually. In +python `x = y` also does declaration, and in JS `let`, `const` and `var` are in place of `int` in declaration, and +this assignment behaviour is wellknown and exists in many of imperative languages. + +In functional languages like Haskell and OCaml, there is `let x = y in f(x)` expression which returns `f(x)`. This is closer +to what we have in rust, it does irrefutable pattern matching, and `let a || let b in c` can be considered as a valid +extension to their syntax. But this isn't a thing in current state of those languages and may look strange a bit at first. + +There are many things that rust is unique in them, specially `if let`, which is the root of let expressions with `bool` type. +Let expression replaces if-let and if-let-chain in list of things that rust is unique in them. + ## Hard to read let expressions Aggressive use of let expressions can lead to complex and hard to read results: From 39ab42d7442d35b2fbfc29a9c3c4d179cba71703 Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Thu, 12 Aug 2021 13:40:47 +0430 Subject: [PATCH 09/19] Add a practical example --- text/0000-let-expression.md | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index ac897d26773..b9046dde2eb 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -102,6 +102,46 @@ assert!(matches!(a, Some(x) if let Some(y) = b(x) && x == y)); assert!(if let Some(x) = a && let Some(y) = b(x) && x == y { true } else { false }); ``` +### Practical usage of this features + +People find let expressions theoretical at first look, and only traditional cases (if-let-chain and let-else) +Can become useful. In this section there are some usages for new features of this RFC in real codes. + +This is an example from rust-clippy repository: +```rust +for w in block.stmts.windows(2) { + if_chain! { + if let StmtKind::Semi(first) = w[0].kind; + if let StmtKind::Semi(second) = w[1].kind; + if !differing_macro_contexts(first.span, second.span); + if let ExprKind::Assign(lhs0, rhs0, _) = first.kind; + if let ExprKind::Assign(lhs1, rhs1, _) = second.kind; + if eq_expr_value(cx, lhs0, rhs1); + if eq_expr_value(cx, lhs1, rhs0); + then { + // 30 lines of code with massive rightward drift + } + } +} +``` + +Which by a generalized let-else can become: +```rust +for w in block.stmts.windows(2) { + (let StmtKind::Semi(first) = w[0].kind) + && (let StmtKind::Semi(second) = w[1].kind) + && !differing_macro_contexts(first.span, second.span) + && (let ExprKind::Assign(lhs0, rhs0, _) = first.kind) + && (let ExprKind::Assign(lhs1, rhs1, _) = second.kind) + && eq_expr_value(cx, lhs0, rhs1) + && eq_expr_value(cx, lhs1, rhs0) + || continue; + // 30 lines of code with two less tab +} +``` +Every `if let` or `if_chain!` that fill body of a loop or function can refactored in this way. You can easily find +dozens of them just in rust-clippy. + ## Why now? This RFC exists thanks to people who choose `if let` for syntax we know today. That syntax wasn't the only choice and there was other options like `iflet`, `if match`, `let if`, `if is` or another `keyword`. From 190325576ffcfe32052754f3d985bda42174819f Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Fri, 13 Aug 2021 01:55:37 +0430 Subject: [PATCH 10/19] Add another practical example --- text/0000-let-expression.md | 42 ++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index b9046dde2eb..c15606c7593 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -43,11 +43,18 @@ This situation is worse with `if let chain` that mix let expressions with `&&` a other bools. In fact the compiler will understand it via interpreting let as an expression, so why we force humans to understand it another way? +Also, this RFC support use case of a new feature, approved but not stablized, called let-else. let-else is not +consistent with if-let-chains and it has taken a completely different path. This RFC can replace let-else +and so remove this inconsistency, without loss in expressive power. + This proposal is also in-line with "everything is an expression" that we have in rust. ## Ergonomics +*Note: This section use new syntax which you may not understand at this point. You +can read [Guide-level explanation][guide-level-explanation] first.* + It also available many super powers for us that can help decreasing rightward drift without adding to implementation and understanding complexity, and actually decreasing it by removing `let-else` and preventing from future similar constructs. @@ -140,7 +147,40 @@ for w in block.stmts.windows(2) { } ``` Every `if let` or `if_chain!` that fill body of a loop or function can refactored in this way. You can easily find -dozens of them just in rust-clippy. +dozens of them just in rust-clippy. Note that if-let-chain alone can't solve this problem, because you have access to +variables of if-let inside of block, but you have access to variables of a binding statement under it, thus you can +save one indentation. + +A different class of practical usages of this RFC is let expression usage as a bool. People +wrap their let expressions with `if expr { true } else { false }` manually. This need is almost met +with `matches!` macro, but `if let true else false` is still a thing in rust code bases. Again from rust-clippy: +```rust +fn is_repeat_zero(&self, expr: &Expr<'_>) -> bool { + if_chain! { + if let ExprKind::Call(fn_expr, [repeat_arg]) = expr.kind; + if is_expr_path_def_path(self.cx, fn_expr, &paths::ITER_REPEAT); + if let ExprKind::Lit(ref lit) = repeat_arg.kind; + if let LitKind::Int(0, _) = lit.node; + + then { + true + } else { + false + } + } +} +``` +After this RFC, we can write this: +```rust +fn is_repeat_zero(&self, expr: &Expr<'_>) -> bool { + (let ExprKind::Call(fn_expr, [repeat_arg]) = expr.kind) + && is_expr_path_def_path(self.cx, fn_expr, &paths::ITER_REPEAT) + && (let ExprKind::Lit(ref lit) = repeat_arg.kind) + && matches!(lit.node, LitKind::Int(0, _)) // you can use let expression here as well +} +``` +Some people may argue that current state is more readable, but [this lint](https://rust-lang.github.io/rust-clippy/master/index.html#needless_bool) +is not agree with them. ## Why now? This RFC exists thanks to people who choose `if let` for syntax we know today. From 1b0a4bfa70d90510d9a5423f37086ecdc59d47d4 Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Fri, 13 Aug 2021 15:20:35 +0430 Subject: [PATCH 11/19] Add another practical example --- text/0000-let-expression.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index c15606c7593..054c8ee3f56 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -151,6 +151,40 @@ dozens of them just in rust-clippy. Note that if-let-chain alone can't solve thi variables of if-let inside of block, but you have access to variables of a binding statement under it, thus you can save one indentation. +`||` is not only useful for let-else style things. This is a real example from [deno](https://github.com/denoland/deno): + +```rust +let nread = if let Some(s) = resource.downcast_rc::() { + s.read(buf).await? +} else if let Some(s) = resource.downcast_rc::() { + s.read(buf).await? +} else if let Some(s) = resource.downcast_rc::() { + s.read(buf).await? +} else if let Some(s) = resource.downcast_rc::() { + s.read(buf).await? +} else if let Some(s) = resource.downcast_rc::() { + s.read(buf).await? +} else if let Some(s) = resource.downcast_rc::() { + s.read(buf).await? +} else { + return Err(not_supported()); +}; +``` +Which by `||` counterpart of if-let-chain can become: +```rust +let nread = if let Some(s) = resource.downcast_rc::() + || let Some(s) = resource.downcast_rc::() + || let Some(s) = resource.downcast_rc::() + || let Some(s) = resource.downcast_rc::() + || let Some(s) = resource.downcast_rc::() + || let Some(s) = resource.downcast_rc::() { + s.read(buf).await? +} else { + return Err(not_supported()); +}; +``` +This is smaller and doesn't repeat a code. + A different class of practical usages of this RFC is let expression usage as a bool. People wrap their let expressions with `if expr { true } else { false }` manually. This need is almost met with `matches!` macro, but `if let true else false` is still a thing in rust code bases. Again from rust-clippy: From 63109164ea84d81cb3e902792fbeb9eb71e782ba Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Fri, 13 Aug 2021 23:11:54 +0430 Subject: [PATCH 12/19] Try to make examples more readable --- text/0000-let-expression.md | 47 +++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index 054c8ee3f56..07a33f3d973 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -61,6 +61,11 @@ actually decreasing it by removing `let-else` and preventing from future similar ### Compare to `let-else` +*This RFC extensively use short circuit operators `&&`, `||`. This operators are called +`andalso`, `orelse` in standard ML and some other languages, reading them in this way +instead of traditional `and`, `or` remind you the short circuit nature of them and help +understanding them better.* + ```rust // simple let else let Some(x) = y else { @@ -79,7 +84,23 @@ let Some(x) = y else { let Some(x) = a else b else c else { return; }; // with let expression -(let Some(x) = a) || (let Some(x) = b) || (let Foo(x) = bar) || { return; }; +(let Some(x) = a) +|| (let Some(x) = b) +|| (let Foo(x) = bar) +|| return; + +// duplicate else block of consecutive let-else +let Some(Foo(x)) = bar else { + panic!("a very long message which needs to change every day"); +}; +let Some((y, z)) = baz(x) else { + panic!("a very long message which needs to change every day"); +}; + +// with let expression +(let Some(Foo(x)) = bar) +&& (let Some((y, z)) = baz(x)) +|| panic!("a very long message which needs to change every day"); ``` ### New constructs @@ -96,10 +117,18 @@ if let Some(x) = a || let Some(x) = b || Ok(x) = c { if let ((Some(x), _, _) | (_, Some(x), _) | (_, _, Ok(x)) = (a, b, c) { // assignment with default -(let Foo(x) = a) || (let Bar(x) = b) || (let x = default); +(let Foo(x) = a) +|| (let Bar(x) = b) +|| (let x = default); // today alternative: -let x = if let Foo(x) = a { x } else if let Bar(x) = b { x } else { default }; +let x = if let Foo(x) = a { + x +} else if let Bar(x) = b { + x +} else { + default +}; // simple let expression assert!((let Some(x) = a) && (let Some(y) = b(x)) && x == y); @@ -318,7 +347,8 @@ merged set of both bindings. So we will have: ``` These are useless alone (equal to separating with `;`) but can become useful with `||`: ``` -(let Some(x) = foo) && (let Some(y) = bar(x)) || (let x = default) && (let y = default2(x)); +(let Some(x) = foo) && (let Some(y) = bar(x)) +|| (let x = default) && (let y = default2(x)); ``` Also, in `EXP1 && EXP2` you can use and shadow bindings of `EXP1` inside `EXP2`. This is because if we are in `EXP2` we can be sure that `EXP1` was true because @@ -326,7 +356,8 @@ otherwise `&&` would be short circuited and `EXP2` won't run. Example: ```rust let foo = Some(2); -((let Some(x) = foo) || panic!("paniiiiiiic")) && (let y = x + 3); +((let Some(x) = foo) || panic!("paniiiiiiic")) +&& (let y = x + 3); println!("{}, {}", x, y); // 2, 5 let a = Some(y); println!("{}", (let Some(b) = a) && b > 3); // true @@ -335,7 +366,9 @@ println!("{}", (let Some(b) = a) && b > 3); // true And you can mix this with normal bool expressions. They have no binding but act like any other bool expression. So we can have this: ```rust -is_good(x) && (let y = Some(x)) || (let y = None); +is_good(x) +&& (let y = Some(x)) +|| (let y = None); // we have y: Option here ``` @@ -353,7 +386,7 @@ assert!((let Some(x) = bar) && (let Foo(y) = x) && y > 2); // no x and y here ``` -Some more complex examples: +Specially, assignments discard bindings of let expressions: ```rust let is_some = let Some(x) = opt; From 9b38567fd4d79f8bf0410498bfda7099f7210897 Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Sat, 14 Aug 2021 02:35:44 +0430 Subject: [PATCH 13/19] Move compare with let-else to let-else section --- text/0000-let-expression.md | 99 +++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index 07a33f3d973..b1c883b14f5 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -52,56 +52,14 @@ in rust. ## Ergonomics -*Note: This section use new syntax which you may not understand at this point. You -can read [Guide-level explanation][guide-level-explanation] first.* - -It also available many super powers for us that can -help decreasing rightward drift without adding to implementation and understanding complexity, and -actually decreasing it by removing `let-else` and preventing from future similar constructs. - -### Compare to `let-else` - *This RFC extensively use short circuit operators `&&`, `||`. This operators are called `andalso`, `orelse` in standard ML and some other languages, reading them in this way instead of traditional `and`, `or` remind you the short circuit nature of them and help understanding them better.* -```rust -// simple let else -let Some(x) = y else { - return Err("fail"); -}; - -// with let expression -(let Some(x) = y) || { - return Err("fail"); -}; - -// or even better -(let Some(x) = y) || return Err("fail"); - -// let else else future possiblity -let Some(x) = a else b else c else { return; }; - -// with let expression -(let Some(x) = a) -|| (let Some(x) = b) -|| (let Foo(x) = bar) -|| return; - -// duplicate else block of consecutive let-else -let Some(Foo(x)) = bar else { - panic!("a very long message which needs to change every day"); -}; -let Some((y, z)) = baz(x) else { - panic!("a very long message which needs to change every day"); -}; - -// with let expression -(let Some(Foo(x)) = bar) -&& (let Some((y, z)) = baz(x)) -|| panic!("a very long message which needs to change every day"); -``` +It also available many super powers for us that can +help decreasing rightward drift without adding to implementation and understanding complexity, and +actually decreasing it by removing `let-else` and preventing from future similar constructs. ### New constructs @@ -140,8 +98,8 @@ assert!(if let Some(x) = a && let Some(y) = b(x) && x == y { true } else { false ### Practical usage of this features -People find let expressions theoretical at first look, and only traditional cases (if-let-chain and let-else) -Can become useful. In this section there are some usages for new features of this RFC in real codes. +People find let expressions theoretical at first look, and think only traditional cases (if-let-chain and let-else) +can become useful. In this section there are some usages for new features of this RFC in real codes. This is an example from rust-clippy repository: ```rust @@ -812,12 +770,57 @@ and they are more consistent with the rest of the language (especially if-let-ch changes for this feature, why make a change just for this particular application? With let expression, this RFC and similar RFCs in the future won't be happen and their task will be taken with this consistent syntax. + +### Compare expressive power of this RFC with let-else + +```rust +// simple let else +let Some(x) = y else { + return Err("fail"); +}; + +// with let expression +(let Some(x) = y) || { + return Err("fail"); +}; + +// or even better +(let Some(x) = y) || return Err("fail"); + +// let else else future possiblity +let Some(x) = a else b else c else { return; }; + +// with let expression +(let Some(x) = a) +|| (let Some(x) = b) +|| (let Foo(x) = bar) +|| return; + +// duplicate else block of consecutive let-else +let Some(Foo(x)) = bar else { + panic!("a very long message which needs to change every day"); +}; +let Some((y, z)) = baz(x) else { + panic!("a very long message which needs to change every day"); +}; + +// with let expression +(let Some(Foo(x)) = bar) +&& (let Some((y, z)) = baz(x)) +|| panic!("a very long message which needs to change every day"); +``` + +### `else` vs `||` + Some people argue that `else` is a better choice and `||` doesn't read very well. But in fact using short circuit operators in this way is a wellknown pattern in general, and it is popular in bash scripting. In standard ML, short circuit operators are called `OrElse` and `AndAlso` which shows that this similarity is known and `else` in let-else is more like `OrElse` rather than `else` in if-else, so `||` is a good choice. -Another benefit is that grammar changes of this RFC are done in if-let-chain and grammar rules of +As a benefit for `||` over `else`, `||` is already in the language and working for +normal bools ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=13b6c01bca742a083b0f48fee07b21d3)). +but `is_prime(x) else { continue };` isn't and won't be valid syntax in rust. So this RFC need much less changes +in the language. In fact, grammar changes of this RFC are done in if-let-chain and grammar rules of `||` and `&&` is simple and wellknown but there are some concerns and special cases about let-else grammar when mixing it with if-else and if-let. From 8d72621d9898d70d97673ef1a763ce6f70f3ea7b Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Tue, 17 Aug 2021 17:08:34 +0430 Subject: [PATCH 14/19] Remove parenthesis from let expressions --- text/0000-let-expression.md | 109 +++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 39 deletions(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index b1c883b14f5..c6b1ad3f33e 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -12,16 +12,24 @@ a bunch of magic constructs that are useful. After this RFC, you'll be able to w ```rust // generalized matches! macro: -assert!((let Some(x) = y) && x > 2); +assert!(let Some(x) = y && x > 2); + +// || counterpart of if-let-chain + +if let Some(x) = foo || let Some(x) = bar { + println!("{}", x); +} // generalized let-else construct -(let Some(a) = b) && (let Some(c) = f(&a)) || { - return Err("failed"); -}; +let Some(a) = b +&& let Some(c) = f(&a) +|| return Err("failed"); println!("{}, {}", a, c); // generalized assignment with default -(let Some(x) = y) || (let Foo(x) = bar) || (let x = default); +let Some(x) = y +|| let Foo(x) = bar +|| let x = default; println!("{}", x); ``` @@ -122,11 +130,11 @@ for w in block.stmts.windows(2) { Which by a generalized let-else can become: ```rust for w in block.stmts.windows(2) { - (let StmtKind::Semi(first) = w[0].kind) - && (let StmtKind::Semi(second) = w[1].kind) + let StmtKind::Semi(first) = w[0].kind + && let StmtKind::Semi(second) = w[1].kind && !differing_macro_contexts(first.span, second.span) - && (let ExprKind::Assign(lhs0, rhs0, _) = first.kind) - && (let ExprKind::Assign(lhs1, rhs1, _) = second.kind) + && let ExprKind::Assign(lhs0, rhs0, _) = first.kind + && let ExprKind::Assign(lhs1, rhs1, _) = second.kind && eq_expr_value(cx, lhs0, rhs1) && eq_expr_value(cx, lhs1, rhs0) || continue; @@ -194,9 +202,9 @@ fn is_repeat_zero(&self, expr: &Expr<'_>) -> bool { After this RFC, we can write this: ```rust fn is_repeat_zero(&self, expr: &Expr<'_>) -> bool { - (let ExprKind::Call(fn_expr, [repeat_arg]) = expr.kind) + let ExprKind::Call(fn_expr, [repeat_arg]) = expr.kind && is_expr_path_def_path(self.cx, fn_expr, &paths::ITER_REPEAT) - && (let ExprKind::Lit(ref lit) = repeat_arg.kind) + && let ExprKind::Lit(ref lit) = repeat_arg.kind && matches!(lit.node, LitKind::Int(0, _)) // you can use let expression here as well } ``` @@ -234,6 +242,9 @@ is late! # Guide-level explanation [guide-level-explanation]: #guide-level-explanation +*Note: Examples in this section are here for showing the corner cases of let expression, not code encouraged or intended to use in real codebases.* + + This section examines the features proposed by this RFC. ## `let` as a bool expression @@ -268,12 +279,12 @@ we will get a compile error. Bindings of result expression is equal to bindings from previous part we have: ```rust -(let Some(x) = foo) || (let x = default); +let Some(x) = foo || let x = default; // we have x here ``` How it will be run? We will reach first line, then: -* If foo matches Some(x), we fill `x` based of foo, `(let Some(x) = foo)` will be evaluated to true, and short circuit the `||` so go to next line. -* Otherwise we will go to next operand, assign default to x, evaluate `(let x = default)` to true and go to next line. +* If foo matches Some(x), we fill `x` based of foo, `let Some(x) = foo` will be evaluated to true, and short circuit the `||` so go to next line. +* Otherwise we will go to next operand, assign default to x, evaluate `let x = default` to true and go to next line. Why their bindings should be equal? Because from knowing that the expression is true, we know one side of the `||` is true, but we don't know which side is true. If their @@ -288,7 +299,7 @@ In addition to `true`, binding statements are allowed to diverge (have type of ` we can write this: ```rust -(let Some(x) = foo) || panic!("foo is none"); +let Some(x) = foo || panic!("foo is none"); println("{}", x); ``` @@ -300,13 +311,13 @@ sense because we don't care about after a return or a panic. If we combine two let expressions via `&&`, bindings of whole expression would be the merged set of both bindings. So we will have: ``` -(let a = 2) && (let Point { x, y, z } = p); +let Point { x, y, z } = p && let a = 2; // we have a, x, y, z here ``` -These are useless alone (equal to separating with `;`) but can become useful with `||`: +These are useless alone (equal to separating with `;`) but can become useful inside if scrutinee (which we don't know yet) or with `||`: ``` -(let Some(x) = foo) && (let Some(y) = bar(x)) -|| (let x = default) && (let y = default2(x)); +let Some(x) = foo && let Some(y) = bar(x) +|| let (x, y) = (default_x, default_y); ``` Also, in `EXP1 && EXP2` you can use and shadow bindings of `EXP1` inside `EXP2`. This is because if we are in `EXP2` we can be sure that `EXP1` was true because @@ -314,9 +325,10 @@ otherwise `&&` would be short circuited and `EXP2` won't run. Example: ```rust let foo = Some(2); -((let Some(x) = foo) || panic!("paniiiiiiic")) -&& (let y = x + 3); -println!("{}, {}", x, y); // 2, 5 +let shadow = 5; +(let Some(x) = foo || panic!("paniiiiiiic")) +&& let x = shadow; +println!("{}, {}", x); // 5 let a = Some(y); println!("{}", (let Some(b) = a) && b > 3); // true ``` @@ -557,6 +569,27 @@ in `|` pattern and silently don't bind `y` in a pattern like `Some(y) | None` so an error until `y` used. But people decided against this (with good reason) and this RFC follow them in this decision. +## Precedence of `||` and `&&` operator + +Currently, precedence of assignment operator `=` is lower than `||` and `&&`, so `let pat = x || y` parse +as `let pat = (x || y)` so parenthesis around `(let pat = x)` is mandatory. Changing this precedence break existing codes +so this RFC doesn't change it in all places. + +`let pat = (x || y)` is only useful when type of `x` and `y` and `pat` are `bool`, because `||`, `&&` can not +be overloaded. detecting type of `x` is not easy in parse level, but few patterns can possibly get bool variables: +* Idents and wildcards +* Bool literals +* `pat1 | pat2`, `ident @ pat`, `& pat`, `(pat)` when inner patterns are bool-possible +* Constants, which are equivalent to idents in parser level. + +If pattern of a let expression isn't bool-possible, parser looks for an expression with precedence +higher than `&&`. It will solve the need for `()` in majority of cases, but not always: +* Constants and enum variants like `let None = opt || x` will need a parenthesis, but they can written via `==` in most of cases. +* Idents, they can be in terminals, but in the middle of expression they need `()`. It shouldn't be popular because `let i = x` is always true. + +It isn't the only corner case of `||`, `&&` and let expressions. Another corner case which is added in +if-let-chain RFC, is that in context of if scrutinee, precedence of this operators is reversed. + ## Divergent case It should be noted that divergent expressions are specially handled. If they happen in top-level @@ -780,20 +813,20 @@ let Some(x) = y else { }; // with let expression -(let Some(x) = y) || { +let Some(x) = y || { return Err("fail"); }; // or even better -(let Some(x) = y) || return Err("fail"); +let Some(x) = y || return Err("fail"); // let else else future possiblity let Some(x) = a else b else c else { return; }; // with let expression -(let Some(x) = a) -|| (let Some(x) = b) -|| (let Foo(x) = bar) +let Some(x) = a +|| let Some(x) = b +|| let Foo(x) = bar || return; // duplicate else block of consecutive let-else @@ -805,8 +838,8 @@ let Some((y, z)) = baz(x) else { }; // with let expression -(let Some(Foo(x)) = bar) -&& (let Some((y, z)) = baz(x)) +let Some(Foo(x)) = bar +&& let Some((y, z)) = baz(x) || panic!("a very long message which needs to change every day"); ``` @@ -820,9 +853,14 @@ and `else` in let-else is more like `OrElse` rather than `else` in if-else, so ` As a benefit for `||` over `else`, `||` is already in the language and working for normal bools ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=13b6c01bca742a083b0f48fee07b21d3)). but `is_prime(x) else { continue };` isn't and won't be valid syntax in rust. So this RFC need much less changes -in the language. In fact, grammar changes of this RFC are done in if-let-chain and grammar rules of -`||` and `&&` is simple and wellknown but there are some concerns and special cases -about let-else grammar when mixing it with if-else and if-let. +in the language. + +For consistency with bool expressions, we should allow something like `let Some(x) = y || panic!("fail")`. For +normal bools it is considered a bad practice, (though [not everyone is agree](https://github.com/rust-lang/rust/issues/69466#issuecomment-591097014) +with it) and explicit `if` is preferred. In let expressions, explicit `if` is not an option due to difference in bindings, +and demand for such construct is real (The presence of let-else is a sign of this), so why we should consider a +pattern that exists in current rust and many other languages a bad practice, and then invent a new construct +that do exactly the same work and read almost the same (let-else vs let-orelse) and consider it a good practice? ## A subset of this RFC Not all things introduced here are useful and some of them are because consistency and completeness. We can make @@ -863,13 +901,6 @@ To be determined. # Future possibilities [future-possibilities]: #future-possibilities -## Changing precedence of `&&` and `||` -Today, as you see in examples, `()` around `let` is mandatory because assignment has lower precedence -than `||` and `&&`. Assigning bools to variables is rare so asking them to put a parenthesis around them -isn't so bad. This is already changed in if scrutinee in `if let chain` RFC as edition `2018`. A future -RFC in edition `2024` or `2027` can change it in other places and make language consistent and remove -unnecessary parenthesis from a then-popular pattern. - ## Convert assignment to a bool expression In [RFC 2909](https://github.com/rust-lang/rfcs/blob/master/text/2909-destructuring-assignment.md) we allow destructing on assignments. A future RFC can make them a bool expression which returns true if From f52c822ad9bc64cee8063ed98ec6be64cbbc150c Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Tue, 17 Aug 2021 18:51:16 +0430 Subject: [PATCH 15/19] Move operator not to future possibilities --- text/0000-let-expression.md | 85 ++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index c6b1ad3f33e..38221cbc89b 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -342,48 +342,44 @@ is_good(x) // we have y: Option here ``` -## Combining with `!` -If we negate a bool expression, all of its normal bindings (which we now call positive binding or PB) become NB (Negative binding) and -its NBs become PB. NBs behavior in `&&` and `||` is reversed. In `&&` they should -be equal and in `||` they will be merged. - -## Consuming bool expressions +## Consuming bool expressions outside of bool operators If we consume a bool expression in anything other than bool operators (such as -function calls or assignments or match expressions) it would lose its bindings. +function calls or match expressions) it would lose its bindings. ```rust let bar = Some(Foo(4)); -assert!((let Some(x) = bar) && (let Foo(y) = x) && y > 2); +assert!(let Some(x) = bar && let Foo(y) = x && y > 2); // no x and y here ``` -Specially, assignments discard bindings of let expressions: - -```rust -let is_some = let Some(x) = opt; -``` -In this example, `is_some: bool` is bound, but `x` isn't and compiler will say it is `unused_variable`. - -Another example: -```rust -let is_foo = (let Some(x) = opt) && foo(x); -``` -And here `is_foo: bool` is bound, `x` isn't and there is no warning because `x` is used in `foo(x)`. Note that if we -remove `()` from `(let Some(x) = opt)` it will becomes `opt && foo(x)` so doesn't compile. - -## `()` vs `{}` - Specially, `{}` expressions will consume bools and lose its bindings. This behavior is consistent with our expectation from `{}` that have bindings only local to itself. So for example: ```rust -assert!((let Some(x) = foo) && (x.is_bar()) || baz == 2); +assert!(let Some(x) = foo && x.is_bar() || baz == 2); ``` -Doesn't compile because of different bindings in `||` (`baz == 2` has no binding but `(let Some(x) = foo) && (x.is_bar())` has `x`) but +Doesn't compile because of different bindings in `||` (`baz == 2` has no binding but `let Some(x) = foo && x.is_bar()` has `x`) but ```rust -assert!({ (let Some(x) = foo) && (x.is_bar()) } || baz == 2); +assert!({ let Some(x) = foo && x.is_bar() } || baz == 2); ``` will compile, because `{}` would discard all of bindings. With `()` instead of `{}` we will get same error of first example. +## Bool operators `!`, `^`, `&`, `|`, `==`, `=` and others + +This RFC reserve usage of bool operators for binding expressions. Originaly, binding rules for `!` operator +was in this RFC. This reservation means expressions like `!x` or `x^y` when `x`, `y` have some binding variables, like +`!(let Some(x) = foo || let Some(x) = bar)` will be rejected by compiler. If you just need bool value of these +expressions and don't expect some bound variable, you can discard binding of expressions with `{}` and use +them like normal bool expressions: `!{ let Some(x) = foo || let Some(x) = bar }`. + +Specially, assignment is an operator so in something like this: + +```rust +let is_foo = { let Some(x) = opt && foo(x) }; +``` +The `{}` are mandatory. Unlike `!` and `^` motivation for `=` isn't reserving for future possiblities, but +for making it consistent with other operators, and more importantly make the fact that assignment +will discard bindings visually clear and reduce confusion. + ## `if` and `while` From definition of `if` we know: @@ -721,18 +717,19 @@ Let expression replaces if-let and if-let-chain in list of things that rust is u Aggressive use of let expressions can lead to complex and hard to read results: ```rust ( - (( - (let Some(x) = a) - && (let Some(y) = x.transform()) - ) || { panic!("failed to get y") }) + ( + let Some(x) = a + && let Some(y) = x.transform() + || panic!("failed to get y") + ) && ( - (let Some(a) = y.transform1()) - || (let Ok(a) = y.transform2()) - || (let Some(a) = if let either = y.transform3() && let Either::Left(left) = either { + let Some(a) = y.transform1() + || let Ok(a) = y.transform2() + || let Some(a) = if let either = y.transform3() && let Either::Left(left) = either { Some(transform_left(left)) } else { None - }) + } ) ) || panic("fun just ended!"); @@ -740,11 +737,11 @@ Aggressive use of let expressions can lead to complex and hard to read results: it can be written on one line, but hopefully rustfmt will prevent that. Also rules of bindings will prevent people to write arbitary let expressions. For example: ```rust -(let Some(a) = y.transform1()) -|| ((let result = y.transform2()) && ((let Ok(a) = result) || { return result; })); +let Some(a) = y.transform1() +|| ((let result = y.transform2()) && (let Ok(a) = result || return result)); println!("{}", a); ``` -won't compile because binding set of `(let Some(a) = y.transform1())` doesn't contain `result`. This rule also +won't compile because binding set of `let Some(a) = y.transform1()` doesn't contain `result`. This rule also make it possible to find which variables will bound with a quick look, that is, every binding variable that appear in a top-level let (not let expressions inside blocks or function calls) will be in the binding set of final expression. so first example will bound `x`, `y` and `a` and we will get it with a quick look. @@ -901,6 +898,18 @@ To be determined. # Future possibilities [future-possibilities]: #future-possibilities +## Binding rules for `!` and other operators +`!` operator was originaly part of this RFC, but because its binding rules was confusing and it wasn't useful in +practical codes, so doesn't pay for its costs, has been removed. + +One possible idea for binding rules of `!` operator is this: +If we negate a bool expression, all of its normal bindings (which we now call positive binding or PB) become NB (Negative binding) and +its NBs become PB. NBs behavior in `&&` and `||` is reversed. In `&&` they should +be equal and in `||` they will be merged. But it is not the only possible idea. + +Similar ideas exist for `^` and other boolean operators. And for operators like `=` need for +unneccesary `{}` can be lifted in a future RFC. + ## Convert assignment to a bool expression In [RFC 2909](https://github.com/rust-lang/rfcs/blob/master/text/2909-destructuring-assignment.md) we allow destructing on assignments. A future RFC can make them a bool expression which returns true if From 0a8b92c753215d8974eae567bb936dd3e82351fd Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Thu, 19 Aug 2021 23:33:12 +0430 Subject: [PATCH 16/19] Update practical examples --- text/0000-let-expression.md | 85 +++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index 38221cbc89b..1df600923ff 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -146,7 +146,59 @@ dozens of them just in rust-clippy. Note that if-let-chain alone can't solve thi variables of if-let inside of block, but you have access to variables of a binding statement under it, thus you can save one indentation. -`||` is not only useful for let-else style things. This is a real example from [deno](https://github.com/denoland/deno): +This pattern which we can call let-chain-else, can also be obtained with the simple let-else: +```rust +let StmtKind::Semi(first) = w[0].kind else { continue; } +let StmtKind::Semi(second) = w[1].kind else { continue; } +if differing_macro_contexts(first.span, second.span) { continue; } +let ExprKind::Assign(lhs0, rhs0, _) = first.kind else { continue; } +let ExprKind::Assign(lhs1, rhs1, _) = second.kind else { continue; } +if !eq_expr_value(cx, lhs0, rhs1) { continue; } +if !eq_expr_value(cx, lhs1, rhs0) { continue; } +``` +And let-else with flavor of this RFC: +```rust +let StmtKind::Semi(first) = w[0].kind || continue; +let StmtKind::Semi(second) = w[1].kind || continue; +!differing_macro_contexts(first.span, second.span) || continue; +let ExprKind::Assign(lhs0, rhs0, _) = first.kind || continue; +let ExprKind::Assign(lhs1, rhs1, _) = second.kind || continue; +eq_expr_value(cx, lhs0, rhs1) || continue; +eq_expr_value(cx, lhs1, rhs0) || continue; +``` +*Do you see boolean algebra here?* This RFC enables reusing code in let-else with equal else block, similar +to merging ifs with equal body or else body via logic operators. This can be specially more useful when +there is something more complex than `continue` like: +```rust +{ + do_something1(); + do_something2(); + continue; +} +``` +You should copy paste it or make it a function (without continue) in let-else example, but let-chain-else has no problem. + +`||` is not only useful for let-else style things. This is a real example from [sentry-cli](https://github.com/getsentry/sentry-cli/): + +```rust +if let Ok(val) = env::var("SENTRY_DSN") { + Ok(val.parse()?) +} else if let Some(val) = self.ini.get_from(Some("auth"), "dsn") { + Ok(val.parse()?) +} else { + bail!("No DSN provided"); +} +``` +Which contains duplicate code `Ok(val.parse()?)`. With this RFC we can write: +```rust +if let Ok(val) = env::var("SENTRY_DSN") || let Some(val) = self.ini.get_from(Some("auth"), "dsn") { + Ok(val.parse()?) +} else { + bail!("No DSN provided"); +} +``` + +Originaly, this code from deno was the example for if-let-or-chain: ```rust let nread = if let Some(s) = resource.downcast_rc::() { @@ -178,7 +230,19 @@ let nread = if let Some(s) = resource.downcast_rc::() return Err(not_supported()); }; ``` -This is smaller and doesn't repeat a code. +Unfortunately, it doesn't compile because types of `s` are not equal. Downcasting in this way is a +popular pattern, and in some cases anonating some `dyn` type can solve the problem. Anyway, if-let-or-chain +with even equal types has many usecases. + +`||` is also useful for assignment with default, specially when `unwrap_or_else` isn't available: +```rust +let size = if let Some(Size(size)) = $v.size { size } else { expand_size }; +``` +Can become: +```rust +let Some(Size(size)) = $v.size || let size = expand_size; +``` +Which is smaller and can better show the intent of operation. A different class of practical usages of this RFC is let expression usage as a bool. People wrap their let expressions with `if expr { true } else { false }` manually. This need is almost met @@ -209,7 +273,22 @@ fn is_repeat_zero(&self, expr: &Expr<'_>) -> bool { } ``` Some people may argue that current state is more readable, but [this lint](https://rust-lang.github.io/rust-clippy/master/index.html#needless_bool) -is not agree with them. +is not agree with them. A more complex example in this category from sentry-cli: +```rust +if let Ok(var) = env::var("SENTRY_DISABLE_UPDATE_CHECK") { + &var == "1" || &var == "true" +} else if let Some(val) = self.ini.get_from(Some("update"), "disable_check") { + val == "true" +} else { + false +} +``` +Which can become: +```rust +{ let Ok(var) = env::var("SENTRY_DISABLE_UPDATE_CHECK") && (&var == "1" || &var == "true") } +|| { let Some(val) = self.ini.get_from(Some("update"), "disable_check") && val == "true" } +``` +this doesn't redefine logic operators. `if x { y } else { false }` is definition of `x && y`. ## Why now? This RFC exists thanks to people who choose `if let` for syntax we know today. From e2e24b97d0d9c1f95aedd215e717ca262f7e6a35 Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Thu, 19 Aug 2021 23:35:03 +0430 Subject: [PATCH 17/19] Fix English grammar error Co-authored-by: fee1-dead --- text/0000-let-expression.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index 1df600923ff..19850afdf3e 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -296,7 +296,7 @@ That syntax wasn't the only choice and there was other options like `iflet`, `if If they chose one of the alternatives, no one would have even imagined the let expressions in form of this RFC and this RFC either did not come into being at all or came in a completely different form and with different capabilities. -But they chosen `if let` among other options (with good reasons) and we are here. Similarly people +But they chose `if let` among other options (with good reasons) and we are here. Similarly people who choose `&&` for `if let chain` could kill this. They didn't choose `&&` as randomly as choosing `if let` and had let expression in their mind, but they had other options like `,` on the table, which wasn't compatible with let expressions. From 7746b6e3b3f6736ea8fddb8df715186e5bd30d9f Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Fri, 20 Aug 2021 02:20:02 +0430 Subject: [PATCH 18/19] Update section Why now --- text/0000-let-expression.md | 44 ++++++++++++++----------------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index 19850afdf3e..e202827fd61 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -291,32 +291,25 @@ Which can become: this doesn't redefine logic operators. `if x { y } else { false }` is definition of `x && y`. ## Why now? -This RFC exists thanks to people who choose `if let` for syntax we know today. +This RFC exists because of the `if let` syntax we know today. That syntax wasn't the only choice and there was other options like `iflet`, `if match`, `let if`, `if is` or another `keyword`. -If they chose one of the alternatives, no one would have even imagined the let expressions in form of this RFC -and this RFC either did not come into being at all or came in a completely different -form and with different capabilities. -But they chose `if let` among other options (with good reasons) and we are here. Similarly people -who choose `&&` for `if let chain` could kill this. They didn't choose `&&` as randomly as -choosing `if let` and had let expression in their mind, but they had other options like `,` on -the table, which wasn't compatible with let expressions. - -But luck is not always with us. We can't expect each new RFC to randomly add another -piece of the let expression puzzle to the language. For example `matches!` and `let-else` are -potentially against this, `matches!` can [coexist with let expression][future-of-matches] but -let-else is not compatible with let expression and they can hardly be together in one place. Fortunately, people have -felt that `let-else` is not compatible with `if let chain`, and this is one of the unresolved questions. The -answer to this question is: they are not compatible! This RFC with less addition to language grammar and -more expressive power is superior. - -So goal of proposing this now is to prevent `let-else` and future similar RFCs to be stabilized. Originally -authors of `if let chain` had an incremental plan toward let expression. -But the implementation of the first part took a long time (which is not over yet) so we are here. If authors -of `if let chain` had submitted a complete proposal from the beginning and it had been accepted, we -would not have seen things like `let-else` at all. +If one of those had been picked this RFC would not have been necessary or would have been very different. +Similary, If the `,` symbol is used instead of `&&` for the if-let-chain RFC, this RFC would not be necessary since it would not be compatible. + +But luck is not always with us. We can't expect each new RFC to add another piece of the let expression puzzle to +the language. For example, `matches!` and `let-else` are potentially not compatible with +this RFC, `matches!` can [coexist with let expression][future-of-matches] but let-else is not compatible with +let expression and therefore can't coexist. Some people have felt that `let-else` is not compatible with `if let chain`, and this is one of the unresolved questions in that RFC. The answer to this question is: they are not compatible! This RFC +has more compatiblity with if-let-chain and less additions to the language grammar. + +The goal of proposing this now is to prevent `let-else` and future similar RFCs to be stabilized. Originally, authors +of the if-let-chain RFC had an incremental plan toward let expression. +The implementation of the first part took a long time and is still not done. Since the if-let-chain RFC is +still not completed we didn't see next steps toward let expressions. So RFCs like `let-else` came to solve problems in their own way, without +compatibility with let expressions and therefore, if-let-chains. Even if it doesn't fit in this year road-map, we should decide if we want it or not today. Even today -is late! +is too late! # Guide-level explanation [guide-level-explanation]: #guide-level-explanation @@ -942,10 +935,7 @@ that do exactly the same work and read almost the same (let-else vs let-orelse) Not all things introduced here are useful and some of them are because consistency and completeness. We can make some subsets of this RFC hard errors and make them future possiblity. Subsets that are candidate of removing are: -* The `!` operator: -we can remove `!` operator so there would be no NB and whole thing becomes simpler to understand. But there may -be some patterns that `!` make sense for them and we don't know today. Also we can lint and warn against unnecessary -usages of `!` operator. But making this hard error can become surprising because we allow negation of simple bools. +* ~~The `!` operator:~~ It has been removed and it is now just a future possiblity. * Consuming let expressions outside of if and binding statements: Some people argue that let expressions in arbitary places can be confusing because the scope of bindings in not clear, and they are useless for common cases (simple matching) in presence of `matches!` macro. From 6e91ca773fc528a64628b7c42430ef88fb9fd1e4 Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Tue, 24 Aug 2021 12:35:21 +0430 Subject: [PATCH 19/19] Fix a problem in examples --- text/0000-let-expression.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/text/0000-let-expression.md b/text/0000-let-expression.md index e202827fd61..d595fa642b0 100644 --- a/text/0000-let-expression.md +++ b/text/0000-let-expression.md @@ -285,8 +285,11 @@ if let Ok(var) = env::var("SENTRY_DISABLE_UPDATE_CHECK") { ``` Which can become: ```rust -{ let Ok(var) = env::var("SENTRY_DISABLE_UPDATE_CHECK") && (&var == "1" || &var == "true") } -|| { let Some(val) = self.ini.get_from(Some("update"), "disable_check") && val == "true" } +if let Ok(var) = env::var("SENTRY_DISABLE_UPDATE_CHECK") { + &var == "1" || &var == "true" +} else { + let Some(val) = self.ini.get_from(Some("update"), "disable_check") && val == "true" +} ``` this doesn't redefine logic operators. `if x { y } else { false }` is definition of `x && y`.