This is subjective. I assume readers of my code are familiar with, and conform to, the Google C++ style guide; you’re from a different school of programming entirely. I don’t think this is at all worth bikeshedding.
It was my impression so far that Rust doesn’t do coercion to bool (for example, if 0 {
isn’t allowed), unlike for example in Python, and I think that fits with the general style of Rust. Similarly, ||
currently only appears to accept bool
(that is, 1 || 3
is not allowed). It seems like a sizable departure from this style to start coercing Option
or other Try
types to bool.
I find this pretty hard to grok. In particular, the asymmetry between the first case being outside of the block and other case(s) being in the block makes it less than obvious to me.
This is no better than just doing
let x = match res {
Ok(x) => x,
Err(e) => something_diverging(e)
};
Which is already valid. The only benefit to yours over this is a few less characters, which isn’t a sizable benefit.
It does with unary/binary ambiguity, like x ?? - y
How about that:
fn func(x: Option<u8>) -> Option<u8> {
let y = x? + 2;
Some(y)
}
?
It would be more like
let x = match x {
Some(x) => x,
None => return expr
};
Where expr
is not necessarily an Option<_>
or Result<_>
.
Hmm. I do like that this avoids the need for ‘magic’ scoping rules as would be required for if !let
, and it’s even more succinct. On the other hand, it feels a bit weird to be bringing a Perlism to Rust.
The parsing issue is interesting. As has been noted, code of the form:
let Foo(x) = expr || expr2;
parses today as let Foo(x) = (expr || expr2)
. But I believe this can never type-check, as ||
always returns a bool
, while a pattern of the form Foo(x)
can never match a bool
. (Handy that we don’t have ‘pattern aliases’, nor do we allow function calls in constant patterns.) So in theory, let
could adjust its precedence based on the syntactic form of the pattern.The only patterns a bool can match are variables, constants, and literals. Variables are irrefutable, so not usable with the proposed new feature. Bool literals can be identified at parse time; bool-typed constants can’t, but neither literals nor constants are particularly useful for this feature since they don’t introduce a binding – so it’s ok if, e.g., let true = x || continue
continues to parse the old way.
For let Foo(x) = expr || expr2
to parse as (let Foo(x) = expr) || expr
, the let Foo(x) =
part would have to have stronger precedence than ||
. For consistency, you would also expect it to have stronger precedence than anything weaker than ||
.
According to the reference, the operators with weaker precedence than ||
are ..
and ..=
, assignment operators (=
, +=
, etc.), return
, break
, and closures. Assignment operators return ()
and can’t be matched against Foo(x)
either. return
, break
, and closures are prefix operators, so they can’t conflict with let
which is effectively another prefix operator. That leaves ..
and ..=
, and they actually do pose a problem, as this compiles:
let std::ops::Range { start, end } = 1 .. 2;
And although std::ops::Range { start, end }
is irrefutable, it cannot be distinguished at parse time from a refutable enum variant pattern. Thus, adding this feature might require breaking that code (at least, it would require significant additional complexity to not break it). On the other hand, building and then immediately destructuring a range is fairly useless and unlikely to be seen in real code. This could also be postponed until an edition boundary.
In any case, a potential alternative is to just require parentheses:
(let Foo(x) = bar) || continue;
I’d say this is uglier, but it arguably has readability benefits, as it’s more obvious that this is not a regular let
, and the || continue
sticks out more.
To clarify, what I think Centril is proposing does not actually involve coercion to bool – unlike the Perl and JavaScript constructs which serve as semi-inspiration. Rather, the idea is that
let Foo(x) = my_enum || continue;
would parse as
(let Foo(x) = my_enum) || continue;
let
would be a bool-typed expression, evaluating to true or false based on whether the pattern matched (sort of retroactively justifying if let
). It wouldn't matter whether the value of my_enum
itself was 'truthy' or 'falsy' or anything like that.
I don’t think you have to do any of this trickery.
Parse it as let <pat> = (<expr> || <expr>);
. Have a transformation that depending on context makes the ||
expand to { let temp = <lhs>; if temp { temp } else { <rhs> }
or let <pat> = match <lhs> { <pat> => <pat>, _ => <rhs>
.
Or if we’re ok with ||
only working with Try
types rather than with refutable patterns, add impl Try for bool
and desugar ||
to (approx) match <lhs>.as_result() { Ok(t) => typeof::<lhs>::from_ok(t), => <rhs> }
.
We have a lot of power to (ab)use ||
(and &&
) since they’re only magic for bool
-typed expressions currently. I’m not sure when they’re “desugared”, but it’s definitely “possible” to intertwine it with type resolution, if not ideal. And the Try
version doesn’t require it.
In any case, I personally prefer magic semantics for ||
over magic semantics for let ||
.
Edit bad thought:
What keeps us from “just” introducing a new statement of form let <pat> = <expr - ||> $(|| <expr - ||>)* ;
that “just” works for bools?
The benefit of parsing it as (let <pat> = <expr>) || expr
is that the desired semantics then become a natural consequence of existing precedent:
-
if let
is effectively precedent for treatinglet
as a bool-typed expression. Of course,if let
is currently parsed as a special case, but withlet
as an expression,if let Foo(x) = bar
can be retconned as just anif
followed by the expression(let Foo(x) = bar)
. -
||
already works with a diverging expression on the right. - The compiler will have to produce an error if you try to use the variables bound in
pat
when the expression on the right of||
doesn’t diverge – but that can be seen as just a special case of the existing initializedness checks. This already works:
But if you replace thelet x; if let Foo(x_) = bar { x = x_; } else { continue } println!("{}", x);
continue
with a non-diverging expression, the compiler complains about a “borrow of possibly uninitialized variable”, as expected.
Thus the language becomes more uniform overall.
Furthermore, it would naturally enable other boolean expressions involving let
, such as this often-requested one:
if let Foo(x) = get_foo() && let Bar(y) = get_bar(x) {
...
}
Or even:
let Foo(x) = get_foo() || let Bar(x) = get_bar() || continue;
although that’s a bit ugly.
That said, I think your first proposal is largely equivalent to mine, other than not supporting the other boolean expressions I just mentioned. You would special-case ||
rather than changing the overall operator precedence, which avoids the ..
problem, but you can do the same thing in the parser – it’s just nicer to keep a consistent precedence. Doing it as a late transformation would allow delaying the decision until after name resolution, so in theory you wouldn’t have the problem with constant boolean-typed patterns – you could allow this to work:
const MY_TRUE: bool = true;
let MY_TRUE = expr || continue;
But as I said, that’s not very useful (and it’s quite confusing), so I don’t think there’s much benefit in allowing it.
That's not my proposal.
let x1 = Some(3);
let x2 = None;
let y1 = x1 || 10; // y1 == 3
let y2 = x2 || 10; // y2 == 10
let z = x2 || z1; // z == Some(3)
This is perfectly consistent with bool
under the type bool = Option<()>; const true: bool = Some(()); const false: bool = None;
isomorphism. And it's lazy just like foo() || bar()
doesn't call bar
if foo
returns true. And it has the exact same precedence as it does with bool.
Hehe; my basic idea was that this would "fall out" from let p = e
just being an expression and the same mechanism would make if !let p = e
work. (let p = e) || continue
is then mostly a matter of style.
Yeah, that's reasonable at least as a stopgap solution that may (or not) be relaxed later.
Yup!
Indeed; I'm working on this atm partially for let_chains
.
Oh? It's quite significant that if !let
would work as well, as it mitigates the "Perlism" issue. But I remember from previous discussions that scoping compared to if let
was a sticking point; I'm interested by the idea of using ||
in part because it sidesteps the scoping issue. How do you intend to address it?
Not only that, but it also avoids parsing being dependent on types, which would be highly undesirable.
Then the following should also just work, right?
if let Foo(x) = my_enum && let Foo(y) = other_enum {
...
}
And with adding a bit of syntactic sugar for ‘let’:
let Foo(x) = my_enum;
let Foo(y) = other_enum;
let Foo(x) = my_enum,
Foo(y) = other_enum;
if let Foo(x) = my_enum,
Foo(y) = other_enum {
...
}
It wouldn’t have to be based on types. In the post you quoted I was trying to explain how it could be done based on the syntactic form of the pattern. But maybe I can be more clear:
- If the pattern is a single identifier,
let
would parse as it currently does. Such patterns are normally irrefutable variable assignments which wouldn’t be useful to use withlet
-as-expression. They can also be names of constants, in which case they’re generally refutable, but also generally unnecessary to use withlet
since you can just use==
instead. You could also use parentheses in that case. - If the pattern is a struct/enum literal like
Foo(bar)
orFoo { bar: baz }
,let
would parse with different precedence – just stronger than&&
. This is okay / not a breaking change because the altered precedence would only make a difference in situations that couldn’t possibly have type-checked in the past – with the possible exception of building and then immediately destructuring aRange
, which is unlikely to be seen in real code. But it’s not necessary to actually do type-checking to make this determination. - Other types of patterns:
Foo::Bar
should probably be parsed the old way but there are some counterarguments; tuple, slice, and reference patterns should be parsed the new way;_
should be parsed the old way;ref
shouldn’t affect things.
Yeah sure; it has been raised many times. For now I want to make progress on Tracking issue for eRFC 2497, "if- and while-let-chains, take 2" · Issue #53667 · rust-lang/rust · GitHub and then we can extend it later
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.