I'm talking about the whole problem, not only when there are two else
clauses. The fact that even deciding whether or not it's ambiguous was hard is a red flag to me.
I do agree that it's not the most readable solution. I'd like to have something a little more readable.
Suggestions welcome for how we can do that without conflicting with existing syntax or introducing any new keywords.
In particular, I'd love to have something that "announces" the presence of the else
before the RHS expression.
I definitely feel this pain. Here's another proposal (perhaps a bit out there):
let Some(value) <= foo else {
}
- + Doesn't conflict with existing syntax (I think)
- + Doesn't introduce new keywords
- + Distinguishes from other syntax before the RHS
- - Introduces new "operator"
- + Doesn't repeat the newly introduced variable name
- ? Clarifies assignment rather than conditional
I like this idiom except that the identifier thing
has to be written three times. Could we introduce some sugar? I don't have good generic design in mind but for example:
let thing = match an_enum {
<= SomeVariant(@),
_ => return Err("err"),
}
By introducing <= $pat
match arm syntax. "@
" is a sigil for the "hole" and result value of the match expression is value of that hole if $pat
is matched.
I just use Kotlin/Groovy name it
: https://github.com/rust-analyzer/rust-analyzer/blob/870ce4b1a50a07e3a536ab26215804acdfc9ba8a/crates/ra_assists/src/ast_editor.rs#L95-L103
Isn't this a job for a macro, à la snafu::ensure?
let_ensure!(SomeVariant(thing) = an_enum, "an_enum should be of type SomeVariant");
// Expands to
let thing = if let SomeVariant(thing) = an_enum {
thing
} else {
return Error("an_enum should be of type SomeVariant")
}
// do something with thing
I'd be wary of using the less-than-or-equal operator for this purpose. Note that <-
is reserved and could be used here.
But if I recall the discussion from way back then, one of the objections was that the following would be valid, but confusing:
let Some(var) = if test() { foo() } else { bar() } else { baz() }
I.e., else
being used twice. Changing the assignment operator won't fix this.
Edit: Just saw @dhm has already brought this up.
That RFC proposes if !let = ...
and if let != ...
syntax. However, this is suboptimal, because the !
is easy to miss when reading code. The proposed if !let
syntax has different semantics and control flow than the existing if let
, so the difference should be more visible.
Another possibility would be a context-sensitive keyword:
let Some(x) = foo or { ... };
or no keyword:
let Some(x) = foo { ... };
Personally, I like the original proposal with else
most.
The fact that even deciding whether or not it's ambiguous was hard is a red flag to me.
There are a lot of possibilities to write code that is hard to understand. When you write unreadable code, that's your fault, not the fault of the language.
This is untrue as-is. When a construct by itself and by default looks confusing, that's definitely a design problem. It's not like there aren't better or worse alternatives in syntax… And anyway, by that logic, why introduce something into the language, just to then instruct people not to use it?
I think you misunderstood me. The following looks ambiguous (at least to a human):
let Some(var) = if test() { foo() } else { bar() } else { baz() };
The following does not:
let Some(var) = foo() else {
baz()
};
Also, we can make the first expression more readable by adding parentheses.
Swift has a quard
statement. It may be worth copying it verbatim, with an edition if necessary. Rust's if let
is copied from Swift already.
guard condition else {
statements
}
Honestly, I prefer spelling it as guard let
(and have suggested as such previously).
I think the current position of the language team (or at least Centril) is to see how far extending let
's usability as an expression gets us. We're getting if let $pat = $expr && let $pat = $expr
currently, and there is rough design for making let
usable more generally as an expression in if
conditions, including naturally supporting if !(let $pat = $expr)
.
(Specifically, the case where the let
"expression" "evaluates" to false
would be required to diverge, and $pat
would be bound in the containing scope, i.e. guard let
.)
As I've said before, I rather like unless
for this, given the diverging requirement. I don't like it in other languages where it's just if!
, but the "block must be -> !
" gives it a reason to exist.
That allows for things like unless i > 0 { continue }
as well, since it's useful to set preconditions for a block for values as well as types. (That's what assert!
does, for example, so it's clearly valuable.)
An advantage of if !let
syntax is that it can be combined with the if let
syntax. For example:
if !let Some(x) = foo && let Some(y) = bar {
// `y` is in scope here
// this block has to diverge
}
// `x` is in scope here
Edit: Actually that idea is broken. Nevermind.
Unfortunately, I don't think that can work. What happens when x = None; y = None;
in that case?
There's nothing wrong if we would only accept ||
and prohibit let
matching alongside with !let
.
This seems to be the "inversion" of principle that if-let-chain uses.
std::ops::Try
is already an option here.
let thing = an_enum?;
If std::ops::Try
was a bit easier to implement, and made a bit generic, then this could become:
let thing = an_enum.info_result::<Ok=SomeVariant>.error_chain(|| "an_enum should be of type SomeVariant")?;
Which is kind of gross, but I think it's a superior direction than relying on the if/else pattern matching when looking to extend the syntax.
It would be nice to allow initializing missing bindings in else
clause instead of diverging e.g.:
if !let Some((a, b)) = x {
a = y;
b = z;
}
or
let Some((a, b)) = x else {
a = y;
b = z;
}
Sounds like the perfect task for Option::unwrap_or_else()
.
I would definitely not like to see code like that in the wild, it's needlessly cryptic.