RFC: let-expression by HKalbasi · Pull Request #3159 · rust-lang/rfcs · GitHub
source link: https://github.com/rust-lang/rfcs/pull/3159
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
New issue
RFC: let-expression #3159
Conversation
Please add a comment in addition to downvote (or upvote existing comments), because comments are more helpful. But feel free to downvote as well ;)
I'm against the now-merged let-else
RFC. But this one is even worse in terms of readability.
For example:
// 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; };
In a general case, if it's not Option::Some
, but Foo
instead:
The let-else
syntax requires user to figure out Foo
in let Foo(x) = a else ..
is a Sum type and thus the binding is fallible. (Otherwise else
doesn't make any sense.)
But with the new syntax, (let Foo(x) = a) || (let Bar(x) = b) || (let Baz(x) = c) || ..
, you're gonna process each and every identity on the left hand side of =
and they must all be Sum types, with exception of the tail of the chain.
For example, this allows the following readability nightmare:
pub mod XXX { pub enum YYY { Foo(i32), Bar(u32) } pub struct Baz (i8); } use XXX::YYY::*; use XXX::Baz; fn nightmare () { let (a, b, c) = (Bar(0), Foo(0), Baz(0)); // Here we go (let Foo(x) = a) || (let Bar(x) = b) || (let Baz(x) = c) || { return; }; }
The reader gotta process the entire expression and internalize that the first two bindings may fail, but then, wait, Baz
is a Product type and the binding never fail. So there's no need for the { return; }
at the end.
This is nowhere being any better for new learners. It's confusing and error-prone.
@lebensterben I don't see your concern. The user can write this today:
if let Baz(x) = c { } else { }
and just get a warning for unreachable code, which is good in my opinion. Irrefutable patterns in let expressions are like x==x
and refutable ones are like x==2
and you can use both of them in ||
and if
and similar. I don't see any catastrophic thing in your example and it isn't semantically wrong or meaningless. It just has unreachable code in || { return; }
which probably will be removed by a compiler warning, before being read.
## 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
fee1-dead 6 days ago
Member
There is something about this RFC that made me unsure about whether this change will be good, however I am pretty sure that it doesn't really matter if let pat = expr
becomes a bool
expression in this RFC. It can be pretty confusing with the PBs and NBs seemingly attached to a single bool
value. Now if we replace all the checking that is needed for bool
returned by let expressions with simply syntactic sugar, we get let else
as well as if let chain
. Is this RFC just about making let
a bool
expression, as well as a wrapper on top of the concepts from if-let-chains
and let-else
?
HKalbasi 6 days ago
Author
The reasoning here is, new learners when see if let
, expect that let
should behave like a bool expression (like anything else inside if
scrutinee) and this confusion will grow if we chain them with &&
in if-let-chain and ||
in let-else. By making them an actual bool
expression, there will be more things to learn (about PB and NB), but there would be no surprise for how let
statement jumped into if
and &&
because it is a bool
expression.
fee1-dead 6 days ago
Member
Do new learners actually expect let
to be a bool expression? The issue you linked in the RFC did not have any mention of bools. And new learners will get confused by the concepts of PBs and NBs too. You clearly have a bias towards if-let-chain and let-else because you describe their syntax as confusing, but as "more things to learn" when you talk about your RFC.
Kixiron 6 days ago
Member
Yah, the linked issue was renamed and labeled as a bug in error reporting, the error wasn't helpful to the end user and had nothing really to do with booleans at all
HKalbasi 6 days ago
Author
Putting ()
around let in if let
has it's roots in expecting if
is a isolated structure and let
is an expression inside it (which should be bool
because it is inside if scrutinee). New learner can soon discover that if let
is another construct and is attached together, but situation is worse with introducing &&
and if-let-chain. I'm not inventing this and this thread in internals has a discussion about confusing let
with an expression. Now there is two approach:
- Try to prevent this confusion, detect errors that mentions let expression as compiler bug (like issue I linked), and restrict new RFCs that use
let
in this way (like if-let-chain) and memorize special cases as different constructs. (current approach) - Make
let
an actualbool
expression. This needs generalizing binding rules to cover if-let and if-let-chain as a subset, and it will probably becomes a complex and confusing rule, but there would be no corner case andif
will become a general thing capable of handling if-let and if-let-chain and more things. (the approach of this RFC and @Centril in that internal thread)
Second approach has costs. But cost of first approach (confusing let
with an expression) is also real and I'm not the only person saying it. And boolean doesn't need to be mentioned explicitly and mentioning let expression is enough, because bool
is the only type that makes sense in context of if-let and if-let-chains.
Show resolved
Outdated
// with let expression
(let Some(x) = a) || (let Some(x) = b) || (let Foo(x) = bar) || { return; };
```
fee1-dead 6 days ago
Member
Please address this concern: aggressively mixing &&
s and ||
s is accepted by the compiler, but it can just be hard to read:
(((let Some(x) = a) && (let Some(y) = x.transform())) || { panic!("failed to get y") }) && ((let Some(a) = y.transform1()) || ((let result = y.transform2()) && ((let Ok(a) = result) || { return result; })) || ((let either = y.transform3()) && ((let Either::Left(left) = either) && (let a = transform_left(left))) || ((let Either::Right(right) = either) && (let a = transform_right(right)))));
beautified:
( ( (let Some(x) = a) && (let Some(y) = x.transform())) || { panic!("failed to get y") } ) && ( (let Some(a) = y.transform1()) || ( (let result = y.transform2()) && ( (let Ok(a) = result) || { return result; } ) ) || ( (let either = y.transform3()) && ( (let Either::Left(left) = either) && (let a = transform_left(left)) ) || ( (let Either::Right(right) = either) && (let a = transform_right(right)) ) ) );
(and it looks like I have mismatched parenthesis)
fee1-dead 6 days ago
Member
That counterargument is pretty weak to me. The second example with "complexity in patterns" is definitely less complex. At least I know the names of variables that are bound immediately by just looking at the first line of let ...
, and I know exactly where each pattern matches on each expression.
HKalbasi 6 days ago
Author
Yes it is less complex, but it is also doing less work. And my point is this complexity can be scaled to infinity and you can write a 10 lines, confusing pattern.
At least I know the names of variables that are bound immediately by just looking at the first line of let ...
It is valid, but discovering which variable is bound isn't hard at all. In fact, all bindings in top-level of expression will be bound. In my example, they are x
and y
and a
. In your example, also result
and either
are in top level, but your example doesn't compile.
I know exactly where each pattern matches on each expression.
Things won't be that easy if I add another |
in pattern:
let ( ((Foo(x), Some(y), (Some(a), _, _) | (_, Ok(a), _) | (_, _, Some(a)), _) | (_, (Bar(y) | Foo(y), Bar(a), (Some(x), _, _) | (_, Err(x), _) | (_, _, Some(x)))
And in let expression we see which expression is in front of which pattern and this help readability. (Maybe I didn't understand your point, if it is the case an example can help)
My argument is "patterns can similarly and equally make infinitely complex examples", maybe I should change RFC text with better examples.
Copy link
runiq commented 6 days ago
Isn't the ||
operator lazily evaluated? Can the rest of my code rely on a let
binding behind a ||
operator?
It is short circuited but since both side of ||
must have equal bindings (both in name and type), we can be sure that at least one of them is bound and it doesn't matter which one is. So rest of code can rely on them.
Regarding "let as a bool expression", it's worth noting that this isn't necessarily the only or most obvious thing a let expression might evaluate to.
Expressions like x = 5
(without let
) evaluate to ()
.
In Javascript x = 5
evaluates to 5
, and similarly with x := 5
in Python. Though this behavior wouldn't be possible in Rust for non-Copy types.
Edit: Somehow I forgot Javascript also has a let
keyword. Doesn't seem let
is an expression there. In Python however there's not a specific keyword for defining new variables.
Copy link
joew60 commented 5 days ago
If I submitted something like
(let Some(x) = a) || (let Some(x) = b) || (let Foo(x) = bar) || { return; };
to a code review; it would be rejected as unreadable. Phrases like "keep it simple" would be used. Even if I understand it today, am I going to understand it in six months?
The fact we can introduce this feature is not a justification for including it.
@joew60 What is unclear in your example that you may not understand it in six months?
The fact we can introduce this feature is not a justification for including it.
Certainly. The justification for this RFC is that it unites features like if-let, if-let-chain and let-else, which are known to be useful, in a general and consistent manner, and then get some extra features for free (with no adding to grammar and concepts of the language). At the end there will be some complex usages (which I don't believe your example is among them) that code reviewers can reject.
If I submitted something like
(let Some(x) = a) || (let Some(x) = b) || (let Foo(x) = bar) || { return; };to a code review; it would be rejected as unreadable. Phrases like "keep it simple" would be used. Even if I understand it today, am I going to understand it in six months?
That's how I feel about most of the examples in the RFC. I think this RFC is too focused on what is theoretically possible instead of what is useful, readable, practical, and realistic. Cramming more logic into a single statement is not an improvement. Code that looks like minified JavaScript is bad code.
I don't feel great about this RFC, but I might consider it more seriously if it had a more practical lens.
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
Comment on lines
+128 to +139
kennytm 4 days ago •
Member
this is more like a practical example of the existing if_let_chains
feature (if you must include a continue
for the number of indentations)
if let StmtKind::Semi(first) = w[0].kind && let StmtKind::Semi(second) = w[1].kind && !differing_macro_contexts(first.span, second.span) ... && eq_expr_value(cx, lhs1, rhs0) { } else { continue; }
this is also more consistent with the way people would write
if next_condition { continue; }
rather than
!next_condition || continue;
HKalbasi 4 days ago
Author
if-let-chain still need one indentation, because variables are bound inside of if body (like if-let) but in the example with ||
variables are bound after statement (like let-else). If you believe if-let can't meet the needs that let-else provide, The same statement is true for if-let-chain and this RFC.
this is also more consistent with the way people would write
I am agree and prefer explicit if
over ||
in normal cases. Without bindings, both behavior is the same (both in current rust and in this RFC) but because binding rules for them are different (for good reasons; for example accessing if-let variables after body is surprising) you can save one indentation by ||
one.
And it is worth noting that !next_condition || continue;
is completely valid and working in today rust. But !next_condition else { continue };
isn't and won't, so let-else syntax has a negative point here and this RFC is more consistent with rust.
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
inquisitivecrystal 4 days ago
Contributor
I think that it's worth adding a helpful error message for this even if the RFC is not accepted. We could even consider making the compiler recover, but with a warning, though I'm not sure that's the way to go. I haven't checked what error message is given currently though.
HKalbasi 4 days ago
Author
I think recovering with a warning is good, especially in if-let-chains because people may want to sure that &&
isn't interpreted wrong so add a (unnecessary) parenthesis. But it will make demand for let expression even more, because users will say: What is this let
that I can put it in if
with parenthesis and &&
it inside if, but nothing else?
@camsteffen I added some practical examples in this section and I probably add some more when I find.
Cramming more logic into a single statement is not an improvement. Code that looks like minified JavaScript is bad code.
This is minified JS: (picked from this page assets)
!function(){"use strict";function e(e){const t=[];for(const o of function(){try{return document.cookie.split(";")}catch(e){return[]}}()){const[n,r]=o.trim().split("=");e=
Is that example really looks like minified JS? What are their similarities? Just because it is in one line, it looks like minified JS? You can put it in multiple lines:
(let Some(x) = a) || (let Some(x) = b) || (let Foo(x) = bar) || { return; };
And the point of this is not count of lines. You can put current alternative in one line as well:
let x = if let Some(x) = a { x } else if let Some(x) = b { x } else if let Foo(x) = bar { x } else { return; };
Or in multiple lines:
let x = if let Some(x) = a { x } else if let Some(x) = b { x } else if let Foo(x) = bar { x } else { return; };
Either way, current solution has unnecessary noise, two times more x
, and let expression solution has more cognitive load because it is a new syntax, but it is clear that what it wants to do.
Maybe you can explain your feelings and what you mean from minified JS by some examples.
Maybe you can explain your feelings and what you mean from minified JS by some examples.
JS is a very flexible language and allows you to write very dense code. Perhaps this is a better fit for the goals of JS. But I don't want to dwell on that comparison. The point is that the code is hard to read. That's just my own subjective feeling from looking at the examples.
@camsteffen minified JS is a machine generated and unreadable by design code, Which doesn't related to that examples I think. JS is flexible but you can minify rust as well. Anyway, it is not our discussion, lets move on.
The point is that the code is hard to read. That's just my own subjective feeling from looking at the examples.
Can you please explain your feeling in more details? These questions may help:
- Is
||
inside if scrutinee hard to read? And if so, what is your feeling about existing if-let-chain proposal? - Are
||
,&&
in top level of a block hard to read? If so, does reading them asorelse
,andalso
like in standard ML make it easier? Problem is chaining them or single||
is bad as well? And do you findelse
in let-else syntax easier to read than||
in equivalent statement? - Do
()
aroundlet
makes them hard to read? And changing precedence of operators help? - Are consumed let expressions in places like
assert!
hard to read andif <expr> { true } else { false }
is better? - Are your feeling for real code examples, equal to you feeling about other ones? Real code examples have more context in them and their variables are not single letter, so maybe they are easier to read.
In each case, if it is applicable, please provide which is wrong, for example bindings are unclear, side effects are unclear, ... It isn't necessary and you can simply don't like anything you want.
Using a top level ||
followed by a block is not necessarily hard to read, but it's definitely surprising because I've personally never seen that in any other language.
To me this:
(let Some(x) = a) || (let Some(x) = b) || (let Foo(x) = bar) || { return; };
is way harder/surprising to read than
if let Some(x) = a || let Some(x) = b || let Foo(x) = bar { return; }
Using a top level
||
followed by a block is not necessarily hard to read, but it's definitely surprising because I've personally never seen that in any other language.
||
followed by some command or block occurs quite often in bash scripts (and Perl too iirc).
How about adding and_then
and or_else
method to bool
? Then we can write:
(let Some(x) = a) .or_else(let Some(x) = b) .or_else(let Foo(x) = bar) .or_else({ return; });
Which in my opinion is much more clear to read and ergonomic to write with autocomplete.
The only problem I can see is that this may potentially muck with operator precedence and some compiler optimizations.
@changhe3 What's the scope for x
then?
@iago-lito The calling scope of course. I don't think function parameters are in their own scope, unless you explicitly use {}
.
Do
()
aroundlet
makes them hard to read?
That is maybe the main problem for me. Each set of parenthesis causes me to pause and say "watch out, the operator precedence is being overriden here..." Lots of parenthesis is also a signal to me that factoring in a variable may be helpful for readability. But that isn't an option here. Rust currently enables you to be very conservative with parenthesis and I like that.
And changing precedence of operators help?
I don't think that is really an option? I have an expecation for the operator precedence of pat = x || y
.
Another issue is that let
is allowed anywhere in an expression. I like that in current Rust, bindings are generally introduced in predictable places - at the beginning of a statement, at the start of a match arm, or in function args. When reading code, the binding names "tell the story". So the readability of a function is highly affected by how quickly I can identify all the binding names defined. With this change, binding declarations are much less prominent.
I don't think that is really an option? I have an expectation for the operator precedence of pat = x || y.
Your expectation isn't met always, and we changed it once in if-let-chain RFC.
I have a very vague and hacky idea to remove need to parenthesis without breaking change: let pat = x || y
only is valid if pat
and x
and y
types are bool. If it is possible to type check x
and if it wasn't bool
, rotate the parse tree to (let pat = x) || y
, the need to parenthesis would be solved. If it isn't possible to change parse tree after type check, we can check if pat
is a bool
pattern without type checking, it should have format of ident
, true
, ident @ false
, and |
combination of this things, so it should be easy to detect it by parse level information. If we find that pat
surely is not a bool
pattern, we can rotate the parse tree. Negative point of the latter is that false positives like ident
is possible and it still mandate parenthesis for let ident = x && y
which x
can be not bool
, but it is better than mandating parenthesis everywhere. (Note that (let ident = x) || y
is non sense and y
is unreachable here.) With both solutions, there is no breaking change.
I don't know if this level of context sensitivity is feasible, and if something like this is done before, but if it is ok, (or ok with changes) let me know to put this in RFC. If it isn't, I should add parenthesis to drawbacks.
at the beginning of a statement, at the start of a match arm, or in function args
And macros. If the problem here is possibility of creating a mess, macros can do that very better, but if concern is that people accidentally make an unclear binding statement that make problem for code readers, I claim that 98% of meaningful binding statements have let
at the beginning (or ((let
) and it can be forced by lint, so you can keep looking at beginning of the statements for discovering bindings.
Do new learners actually expect let to be a bool expression?
I personally would expect that.
I don't feel great about this RFC, but I might consider it more seriously if it had a more practical lens.
I don't understand the intricacies here, but as a +1: I would definitely find it nice to be able to write:
if let Some(x) = self.some_func() && let Some(y) = self.other_func() {}
instead of having now to do
if let Some(x) = self.some_func() { if let Some(y) = self.other_func() {} }
It would reduce the number of braces, which is objectively better.
I realize this is possible sometimes, but it's not as readable
if let (Some(x), Some(y)) = (self.some_func(), self.other_func()) {}
It's not possible when the output of some_func
needs to be piped into other_func
. The workaround being:
if let Some((x, Some(y)) = self.some_func().map(|x| (x, self.other_func(x))) {}
Which is not as readable as well.
Not a fan of:
... || { return Err("failed"); };
I feel like that should be a default clippy warning. It's bad style, but imo shouldn't block this feature.
as a +1: I would definitely find it nice to be able to write
That is not something coming from this RFC, see if-let-chains which is already an accepted RFC.
No one assigned
None yet
No milestone
Successfully merging this pull request may close these issues.
None yet
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK