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

Could someone point to a representative example of what the guard statement even looks like? Seems like the entire thread is filled with people that already know Swift or something, since there's not a single example showing the suggested syntax. :wink:

6 Likes

I'm not at my main computer right now and this code may not actually compile (although it should hopefully be close enough to give an idea). It's also super contrived and folks likely wouldn't write code quite like this - for basic illustration purposes only!

func doSomething(var1: Double) -> SomeType {
    // guard-ing a Boolean condition
    guard var1 > 0 else {
        // Any arbitrary code can go here, for example:
        print("var1 must be greater than 0")
        // ...etc...
        return // The compiler will complain if this return is missing - the body of the guard must diverge
    }

    // guard-ing an assignment with let (similar to Rust's let-else)
    guard let var2 = someFunc() else {
        // As above
        print("var2 could not be computed")
        return
    }

    // "Happy path" here
    // ...etc...
}

Basically, the guard statement provides a clean, compiler-checked way to do early returns (and more). You'll often see it mentioned with reference to "avoiding the pyramid of doom" and "keeping the happy path to the left".

It can result in flatter-looking functions that can be more readable than their deeply-nested counterparts.

You can of course fairly much do exactly the same thing using plain old if but the nice thing about guard is that the compiler will ensure you you are diverging - such as via the returns in the above example. So you can't accidentally forget to include the return like you could with an if condition that's attempting to return early.

guard also works in analogous ways inside of loops - for example, you'd test some kind of condition at the start of the loop and then break or continue if it's not met (again, the compiler would ensure you do this), and then the main body of the loop is free to assume all the problem code is out of the way with.

I'm not sure if that explains it well enough? It's kind of if...return with extra compiler checks, or alternatively it can be thought of as let-else but more general.

It's this more general alternative to let-else that I was originally enquiring about, and @m-ronchi 's suggestion above actually gets me pretty close. Although to cut a long story short it might all come down to me needing to lean into Rust's idioms more and forget about this whole business! Especially with some folks mentioning above that they're not actually fans of let-else in Rust.

That helps, thanks. So it indeed seems fully equivalent to this, then (which has been suggested above):

let true = condition else {
  // Code goes here. Compiler ensures you are diverging on this branch.
};

The Rust version looks really strange, but is not actually that much more verbose than guard -- it's literally let true = vs guard.

In your 2nd example, what is the return type of someFunc()? Is it the equivalent of Option<T> and this the same as let Some(var2) = someFunc() else { ... }, just somehow with Some being implicit?

You say this comes "pretty close", what is the remaining difference?

FWIW I quite like it. :person_shrugging:

2 Likes

Yep that’s right re: the second example, exactly.

And really I think by “pretty close” I was just talking about the slightly strange syntax involved with the Rust version, which almost (to me) feels like an abuse of the let statement. But other than that it’s really the same thing, yeah. I think I just need to look at it a few more times to appreciate its beauty!

And I’m glad to hear there’s somebody else out there that quite likes let-else! Thank you!