`if let ... where ...`

This is a proposal complementing let chains and the is keyword.

Both existing proposals try to solve the problem of putting chains of interdependent bindings into a single if statement. In my opinion, allowing to place bindings at arbitrary positions in if statements leads to less readable code. Chains of interdependent bindings should be written as multiple statements. The problem of deep indentation caused by this should be solved in a different way. Therefore I propose to stick with placing bindings only in the beginning of a statement.

I propose to add an optional where clause to an if let or while let statement. The where clause can access the bindings made in the let part of the statement.

Let's extend the example from the is RFC:

if expr_producing_option().is_some_and(|v| condition(v))

if let Some(v) = expr_producing_option() && condition(v)

if expr_producing_option() is Some(v) && condition(v)

// New idea 
if let Some(v) = expr_producing_option() where condition(v)

Given that the examples do not get more complex in the is RFC, this proposal of introducing if let ... where ... would solve them all. And it does that by using common Rust syntax and principles.

Hence, I propose to consider using the if let ... where ... construct in place of is.

1 Like

Which is the difference between

if let Some(v) = expr_producing_option() && condition(v)

and

if let Some(v) = expr_producing_option() where condition(v)

?

If they're the same, the first seems more intuitive to me.

5 Likes

This feels like overloading the where keyword. The trait system is hard enough to teach, I see no need to use where for additional purposes besides the “where clauses” we already have. (It would also make searching for “where” in Rust harder.)

11 Likes

I see your concerns. Overloading where in particular would make sense from a "natural language" point of view, in my opinion. In particular, it would lift where from "specifying trait bounds" to just specifying bounds in general. But in the end the actual term actually matter less to me.

What matters more is that we do not overload the && operator. This operator is commutative and can be chained, and hence is not the right candidate for restricting bindings to the beginning of an if let statement. Specifically,

if let Some(v) = expr_producing_option() && condition(v)

is not equal to

if condition(v) && let Some(v) = expr_producing_option()

And also, in my opinion, expressions with multiple separate let bindings should not be allowed for reasons of readability. But && can be chained, and it would not be obvious that let Some(v) = a() && let Some(u) = b(v) is not allowed.

Then there is the argument that I have seen being used here and there, that a binding operator should not return any value. But using && with if let implies that the let statement returns a boolean value. This is not my argument, but other people maybe know better about this.

1 Like

&& is not commutative, false && panic!() is not the same as panic!() && false.

(Technically I don’t think any operator in rust is commutative in all situations, impl Add<T> for U and impl Add<U> for T don’t have to do the same thing).

15 Likes

Because it is, in fact, allowed. The let-chains feature that allows let Some(v) = a() && ... in the first place specifically enables let Some(v) = a() && let Some(u) = b(v).

9 Likes

Apologies for dwelling on the specific term, but on first read I thought it it'd be natural and consistent (though confusing taken in isolation) to use if, to better mirror the concept of if let being a single-arm match

if let Some(v) = option_producing() if condition(v) {
    // ...
}
// as identical to
match option_producing() {
    Some(v) if condition(v) => {
        // ...
    }
    _ => {}
}

I thought this, too; one could make an alternative let-chain proposal that replaces all the && with if. Could even be fun to help with alignment

if let Some(x) = foo()
if let Some(y) = bar()
if cond(&foo, &bar) {
    do_action(foo, bar);
}

and then for while let though… eh…

while let Some(x) = foo()
if let Some(y) = bar()
if cond(&foo, &bar) {
    do_action(foo, bar);
}

not so neat anymore. And that’s probably a huge issue with using “if” here: You get

while (COND1) if (COND2) { 
    action();
}

with vastly different meaning than

while (COND1) {
    if (COND2) {
        action();
    }
}

which is quite bad IMO, unless your goal is to confuse all the C programmers.

5 Likes

This was one of the options considered in the original let-chains proposal, and people polled found it confusing as well.

5 Likes

I think that your statement is true but I also think that I do not want to strive for any natural language resemblence. I would much rather prefer to learn a language which is concise in its wording and not conflicting itself and mixing terminology between conceptually different aspects.

To me the where keyword is exclusive to constraints on generic types and I would like to keep it in this realm. Mixing this with information which is not obtained during compile-time but rather runtime is confusing to me.

3 Likes

True. I would argue though, that for most cases, code should not rely on boolean short-circuit evaluation or the order of evaluation of a boolean expression. It hides control flow and hence goes against being explicit.

I see. My original proposal was to specifically not allow such chains. Hence a different keyword than just &&.


But yeah, if people want to chain lets in a single if, then my proposal does not help. In that case, one could still replace the if let A(a) = b() && x(a) with an if let A(a) = b() with x(a).

This is to make x(a) a substatement of if let A(a) = b() with, which makes it clear that the order matters, just by the design of the syntax. Also, it does not require let to return a boolean value.

It prevents using || in place of with though, which may or may not be desired in the future. Also, making let boolean would be more powerful. "Weird" uses could be flagged by clippy.

Putting purely-for-side-effect expressions in && is nonidiomatic, yes, but using && in an ordered manner is perfectly acceptable. It's less common in Rust due to the parse-don't-validate design mindset and lack of let chaining, but writing if i < n && arr[i].condition() {} is generally considered preferable to if i < n { if arr[i].condition() {}} or if (i < n).then(|| arr[i].condition()).unwrap_or(false) {} or if (i < n ? arr[i]. condition() : false) {}.

You can't logically argue against using a && b as control flow but for using a ? b : c control flow, because they're the same class of control flow with similar affordance for and explicitness of being control flow.

And I feel the need to reiterate: explicitness is explicitly not a goal of Rust. The actual goal of Rust is the similar but distinct goal of locality (composability) of reasoning. This is often shorthanded as explicitness, as implicit semantics usually relies on nonlocal reasoning, but explicitness is just a means (and not the only way) of accomplishing localizable reasoning, not a goal itself.

7 Likes