Swift-like guard statement

(First post here - many apologies if this is not the best place for it!)

Rust has let-else, which is quite similar to Swift's guard-let, and which is something I use all the time.

But are there any plans to add something equivalent to a more general guard statement?

Basically the equivalent of:

if !some_condition {
    return // or continue if in a loop, or panic!(), etc
}

The difference being that guard enforces divergent / early return behaviour, in the same way that let-else does, and so carries some extra semantic meaning (and compiler checks) with it over the plain if. So this is kind of let-else but without the assignment aspect.

1 Like

ugly, but

let true = some_condition else { return; };

works

6 Likes

anyhow has ensure! macro which does a similar thing (return Err if the condition fails). Other error-handling crates have similar functionality, and it's easy enough to write the relevant macro yourself. I don't see a reason to make it a language built-in syntax.

Similarly, assert! in the standard library serves the same purpose for panic!().

Note that depending on your use case, you may want different behaviour in the divergent branch. Could be an early return, could be a break or continue. Imho the value-add of special syntax is thus quite low. If it's complex enough to cover all those use cases, it won't be particularly ergonomic, and if it isn't, you'd have to use different approaches anyway. Specifically in the case of early returns, a common case is returning Err(Error::Variant). You'd generally want to abstract away the repeating return Err(..); part, but have some control over the construction of errors (which may themselves have some repeating part amenable to a macro).

3 Likes

Thanks. I had thought I may have a go at creating a macro to do it but wasn't sure if I could cover all bases.

I've realised my sample code was probably far too basic, and probably should have looked something more like this:

if !some_condition {
    // potentially any extra logic here
    // ...
    // ...

    return // ultimately culminating in the compiler-enforced return/etc
}

Swift's syntax covers it fairly elegantly, but I can appreciate it may not be a good fit for Rust and that there are macro alternative that can essentially do the same thing.

Thanks for the pointers and reading material!

This is quite interesting actually!

Might take a while for my eyes to get used to it, but it ticks the box - thanks!

The Perlish pattern

some_condition || return;

works as well, though it generates a warning about an unused value. YMMV whether it’s ugly or not.

2 Likes

Wrapping that up in a (possibly?) easier on the eye macro seems to be working quite nicely for me now:

macro_rules! guard_else {
    ($condition:expr, $body:block) => {
        let true = $condition else { $body };
    };
}

and to give a hideously contrived example:

for i in 1..=100 {
    guard_else!(i < 10, {
        // Arbitrary logic here...
        println!("DONE");
        break; // Compiler complains if this is missing (which is good!)
    });
    // ...
 }

This is very close to what I was originally envisaging and in very little code. Thank for the insight on using let-else in that way!

1 Like

Rust already has if let (soon hopefully with multiple let) and let else, and I think further combos of these are more and more niche, and overload the syntax more and more.

After using let else for a while, I'm not even that enthusiastic about it any more, because I often find that I do want to inspect the error, and have to change to a match anyway (and all the proposal to add extra patterns to let else look like match with some randomized syntax order).

The if !condition { return } construct is as old as if and return, so inventing something new for it would be adding weirdness to the language. Maybe ? could be implemented on bool :slight_smile:

5 Likes

Thanks, this is an interesting take for sure and does give me pause for thought.

guard in Swift is generally considered to be an extremely useful tool for early returns (and similar), and avoiding deeply nested conditions - but I can certainly appreciate that Rust is not Swift and that this just might not be a good fit.

My experience currently lies more on the Swift end of the “Rustometer” after working with it for nearly a decade, so I probably need to fully internalize the Rust way of doing things a little more. It might well be I end up favouring match over let else after another 6 months perhaps!

TBH, I wish we'd done unless instead of let-else, since I'm also finding myself more and more saddened by let-else. Notably, when there's a whole bunch of lets that all have else { continue }; or similar, I wish there was something better.

unless let Some(x) = y
    && let Some(z) = x.blah()
    && z >= 0
{
    return None;
}

would have chained better, and could have kept the "body must diverge" property to give it a reason to exist over just if !.

(But who knows, maybe we'll be allowed to put chains in let one day for that use, sharing an else. That'll still be weird for any conditions that aren't a let, though.)

3 Likes

I've always found it funny to imagine allowing pattern-guards in let-else

let Some(x) = y && let (Some(z) if z >= 0) = x.blah() else {
    return None;
}

IIRC, it’s exactly for that possibility that && used directly in let else is currently not allowed.

let x = true && false else { … };
error: a `&&` expression cannot be directly assigned in `let...else`
 --> src/lib.rs:2:13
  |
… |     let x = true && false else { … };
  |             ^^^^^^^^^^^^^
  |
help: wrap the expression in parentheses
  |
… |     let x = (true && false) else { … };
  |             +             +


Or was that just because it was considered somehow potentially confusing?


Edit: Indeed let-chains were are mentioned as future possibilities in the let … else RFC. A let … else match is also mentioned, by the way.

https://rust-lang.github.io/rfcs/3137-let-else.html#future-possibilities

Sorry, but I got to ask: Are you familiar with the ? operator?

Yes thanks but I’m not sure it enables the kind of patterns I had in mind, particularly to do with running arbitrary (and local) code prior to the early return from a function or break/continue in a loop