RFC idea: let ... else, easy early returns when pattern matching

There's also this: https://github.com/rust-lang/rfcs/issues/2616

2 Likes

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.

1 Like

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?

1 Like

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.

3 Likes

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?

3 Likes

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.

1 Like

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;
}
2 Likes

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.