(Also, I don’t see why "Ambiguity when rhs of = is in form if { () }" would be an issue in practice. if has this ambiguity already and else is attached to the closest if when ambiguous, but I haven’t seen any complaints about it.)
OK, I’ll go into this a bit!
Rust does not have the classic dangling if ambiguity. In Java, it looks like this:
if (condition)
if (condition)
statement();
else
statement();
I’ve half-indented the else here to signify that it could legally be attached to either if and still satisfy the grammar. The language specifies that each else attaches to the innermost if.
This only occurs because of block-less if. In rust, if $expr $expr is invalid. Instead, you are required to use a block as follows:
if condition {
if condition {
statement();
}
else {
statement();
}
}
Even though I’ve mangled the indentation here, it’s still unambiguous which if the else is attached to, because of the required {} block.
The interesting case, though, is that the following is valid code (proof):
let () = if true { () };
let () = if false { () };
(Note that this is not valid code when the type in the if expression is anything other than (), so this is effectively an extremely useless edge case of the grammar that this is allowed. But the argument against introducing ambiguity into the grammar doesn’t care how edge-case-useless an ambiguity is, having one is bad.)
The only logical way to resolve the ambiguity is to attach it to the if. The only time not doing so is when if EXPR BLOCK is legal in expression position, which is only when it has type (). In any case, that’s required to prevent breaking so much valid code today in form
let value = if rng { a } else { b };
I’ll show the ambiguity visually by abusing parenthesis:
let (value = if rng { a }) else { b } ;
let value = (if rng { a } else { b });
Avoiding introducing ambiguities into the grammar is not about how obvious the correct choice is, but about not having ambiguities in the grammar. Assuming Rust’s grammar doesn’t have any ambiguities (I know there has been effort to avoid them but who knows if any sneaked in), it’s best to avoid introducing new ones.
And just to visually show let ... else with an rhs of if ... else:
let Ok(result) = if rng {
let Inputs(i, n, p, u, t, s) = get_algorithm1_inputs();
algorithm1(i, n, p, u, t, s)
} else {
let Inputs(i, n, p, u, t, s) = get_algorithm2_inputs();
algorithm2(i, n, p, u, t, s)
} else {
// this one is required to diverge / have type `!`
log!("Failed to do something important!");
bail!(FailureCase::UnexpectedFailure)
}
Personally, I don’t use “complicated” expr rhs’es on my if let, and probably wouldn’t on my let ... else either, so this is going to show up less often. That’s not a reason to ignore it, though; if anything it’s a reason to pay more attention to the edge cases, because they’ll be seen less often and you want them to be understandable at a glance.
(This got circular I’m cutting it off here)
If if condition { block } is extended in the future to mean if condition { Some(block) } else { None }, the ambiguity becomes more meaningful, but still would be required to attach to the if rather than the let to preserve the semantics of existing code. But that’s a completely off-topic separate language extension idea.