There's also this: https://github.com/rust-lang/rfcs/issues/2616
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.
What about the case of a large enum with many cases? It's not exactly practical in all cases to provide as_variant(&self) -> Option<&Variant>
and into_variant(self) -> Option<Variant>
for every variant, let alone unwrap_variant(self) -> Variant
, unwrap_variant_or(self, Variant) -> Variant
, unwrap_variant_or_default(self) -> Variant
, unwrap_variant_or_else(self, impl FnOnce() -> Variant) -> Variant
.
Option
is just used as a standin for large enumerated types, often in the control of an upstream dependency. Saying "just use Option
combinators" is dismissing the problem as solved for one specific standard type.
The standard pattern is for an enum that expects this kind of manipulation a lot to provide as_variant
/into_variant
, and using Option
combinators on that for non-diverging control flow (or Try
-diverging via ?
), and in all other cases,
let (these, real, names) = match it {
It::Variant(these, transient, names) => (these, transient, names),
_ => otherwise(),
};
I think I agree that a refutable let should enforce diverging; a big (claimed) benefit of Swift's guard
is that you know that the block is a diverging error handling case. But saying "just use Option
" is overly reductive.
For more complex types I'd be perfectly happy with if let Variant(v) = expr { v } else { default }
.
(If it came up very frequently, I'd probably write a helper method for it anyway, because it's duplicated logic even with let-else sugar. I like my code to evolve organically; I don't expect people to provide methods for every possible use case upfront, nor do I do that myself.)
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.