Clean conditional return statements

I've noticed for the past few years whilst programming in rust that there is no clean conditional return for instance: when a function needs to exit early in rust:

if condition {return}

other languages:

if(condition)return;

The rust syntax feels kinda clunky when it comes to things like that. This also happens for conditional breaks and continues but are less of a problem as they are rarely needed. Now I wanted to get some of your opinions about a syntax like this:

return if condition;
return value if condition;
continue if condition;

or even this syntax for the return if true:

rift condition;

If you have any ideas or concerns about this please let me know because I would love for rust to have something like this.

1 Like

An extra pair of braces imho is ok for readability. You still have the extra parenthesis in if(cond)return;. Or if you're talking about rustfmt automatically splitting it to two lines, actually clang-format does the same for C code.

A simple alternatives would be to write a macro rift!(cond); that expands to if (cond) return;, or use Result so you can early exit with a single ?.

7 Likes

You could always just define your own macro if you want different syntax

macro_rules! rtif {
    ($condition:expr) => {{
        if $condition {
            return
        }
    }}
}
fn demo() {
    rtif!(condition());
}

Rust Playground


I don’t see much value in new syntax here though. Usual, relevant concerns solved by new syntax sugar are too lengthy syntax, and/or added levels of indentation. I don’t see any such concerns with existing if condition { return; }. The only possible problem I see is that the normal way rustfmt formats it adds a number of extra lines, turning

if condition() { return }

into

if condition() {
    return;
}

If that’s the problem to be solved, perhaps (if it doesn’t exist already) a way to configure rustfmt differently is all that’s necessary?

14 Likes

Thanks for your reply. When comparing readability between these three functions:

pub fn jump1(input:Res<Input<KeyCode>>){
    return if !input.just_pressed(KeyCode::Space);
    //... jump logic
}
pub fn jump2(input:Res<Input<KeyCode>>){
    if !input.just_pressed(KeyCode::Space) {return}
    //... jump logic
}
pub fn jump3(input:Res<Input<KeyCode>>){
    if input.just_pressed(KeyCode::Space) {
        //... jump logic
    }
}

I would argue that the first is the most readable that is why I would propose a return if condition syntax

I have a hard time imagining where simply returning is the correct way to handle conditions. Those cases exist, but you'd almost always want to return some specific value if you do an early return, like the ? operator does for error handling. If you're returning a value anyway, a couple of braces make no difference for readability.

This makes me think that you over-rely on side effects, including global mutable state, instead of properly propagating data between functions. That's not a pattern which I would want to encourage in Rust.

An example of more structured early returns is the anyhow::ensure! macro. Most of the time that's the preferred solution, and you can write a similar macro if you're doing a lot of early break or continue calls.

10 Likes

single_line_if_else_max_width ?

1 Like

yeah I should have specified that I didn’t meant for this to discourage proper data propagation. but more simply for filtering logic in systems like the in the code my reply to Hoblovski

Yes it's a bit more readable but I'd go with the macro as steffahn wrote.

Also I'm not sure if adding return if causes some trouble for the parser, because it has to distinguish between your return if with code like

fn f2() -> usize {
    return if true { 20 } else { 30 };
    // return if { true { 20 } else { 30 } };
    // or 
    // return { if true { 20 } else { 30 } };
}

Correct me if I’m wrong but from my understanding a if loop in rust requires a {} body, in which case the parser could differentiate between the two statements

I can't say since I'm not familiar with parsing in rustc but it looks like rustc uses a recursive descent (pretty much like PEG) way. Just from my experience with PEG parsing for my toy languages, parsing is quite subtle and can break very unexpectedly, especially with two different constructs sharing the same prefix.

1 Like

Does that apply to if without else?

Here’s a guard function usable on () return types with ? operator using the (unstable) Try trait.

fn demo() {
    guard(condition())?;
}

Rust Playground

With this, the above example looks like

pub fn jump42(input:Res<Input<KeyCode>>){
    guard(input.just_pressed(KeyCode::Space))?;
    //... jump logic
}
4 Likes

Yess I was just thinking that could the Try trait be implemented directly on a bool?

I tried making it work with Option, too, but ran into

impl<T> FromResidual<GuardFailure> for Option<T> {
    fn from_residual(_: GuardFailure) -> Option<T> {
        None
    }
}
   Compiling playground v0.0.1 (/playground)
error[E0119]: conflicting implementations of trait `FromResidual<GuardFailure>` for type `Option<_>`
  --> src/lib.rs:18:1
   |
18 | impl<T> FromResidual<GuardFailure> for Option<T> {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: conflicting implementation in crate `core`:
           - impl<T> FromResidual for Option<T>;

That seems like a bug / undesirable error, right? cc @scottmcm

In case that’s an unavoidable or not-going-to-be-fixed-anytime-soon consequence of overlap detection on an impl involving associated types, such as the FromResidual for Option<T> one in the standard library which desugars to FromResidual<<<Option<T> as Try>::Residual>> for Option<T>, then perhaps, the “convenient” R = <Self as Try>::Residual default type parameter on the FromResidual trait is a bad idea?

For the specific case of bevy systems, as an alternative to

fn do_jump(mut q: Query<...>, input: Res<Input<KeyCode>>) {
    if !input.just_presses(JUMP_KEY) { return ]
    // do jump
}

schedule.add_system(do_jump);

you can use run conditions instead:

fn do_jump(mut q: Query<...>) {
    // do jump
}

fn should_jump(input: Res<Input<KeyCode>>) -> bool {
    input.just_pressed(JUMP_KEY)
}

schedule.add_system(do_jump.run_if(should_jump));

utilizing the new stateless ECS scheduler coming with 0.10, or iyes_loopless for 0.9.

3 Likes

Yeah I know I'm stoked for the new bevy release

Funny stuff. So apparently

let _ = if condition() { return } else { () };

goes into a single line. But remove the let _ or the else or even just the () inside of the else and you get multiple lines...

(Something like

(if condition() { return } else { () });

works, too, staying in a single line. Of course any such workaround for rustfmt would be stupid to actually use)

Maybe?

What specific part is the issue? Is it fixed if

is replaced with this?

impl<T> const ops::FromResidual<Option<convert::Infallible>> for Option<T> {

Or maybe WG-trait-system-refactor will change how this works anyway...

I'm not sure which part of associated types you mean here,

I'll still need to test if that actually fixes it, but yes, that was the idea of what I believe could fix it.

The associated tyle I'm referring to is the associated type <Option<T> as Try>::Residual appearing as the type parameter in the impl FromResidual<<<Option<T> as Try>::Residual>> for Option<T>.

I think jump2 is the most readable. Mentally I'm doing a lot more gymnastics to figure out whether jump1 is a conditional return or whether the if expression returns a value that gets returned. If I'm skimming code, I'm much more likely to make a mistake with jump1.

In a language where "if" is a statement and never an expression, this isn't a problem. Rust already deviates a bit with the way it handles implicit returns and such, I'd be very wary about adding another layer of complexity on-top of that.

2 Likes