Syntactic sugar for `if x { return | break | continue }`

(I know you're already aware, but) for the info, it's possible to use let else to get unless behavior by using a yoda conditional, e.g.

let true = x > 0 else { continue };

This definitely falls under "cute tricks you probably shouldn't use," but it is still a cute trick.

11 Likes

I know this isn't quite as powerful as what you proposed, but if this proposed feature's main focus is on returning Errors, couldn't this just be a function on the Result and/or Option types?

Err(SomeError::Forbidden).unless_err(!may_i_do_this)?;

where .unless_err() returns Ok(()) unless the Result is Err and the predicate isn't satisfied.

My experience with Ruby tells that, when used tastefully, code feels more natural and readable with this postfix if (but only if it all fits in a single, short line). In this sense this is like fluent interfaces (chained method calls).

Like many features (specially "there's more than one way to do it" features), this one is only helpful if people use it correctly.

1 Like

res.unless_err(pred) is

res.or_else(|e| pred.then(||{}).ok_or(e))

But if you're just throwing Err(_) at it ...

Err(SomeError::Forbidden).or_else(|e| may_i_do_this.then(||{}).ok_or(e))

...you might as well write...

may_i_do_this.then(||{}).ok_or(SomeError::Forbidden)?;

or if you really want the other ordering, you can implement a method on your error type.

// fn err_unless(self, pred: bool) -> Result<(), Self>
SomeError::Forbidden.err_unless(may_i_do_this)?;

Yeah there are a lot of ways to do it, it is just

if predicate { return Err(SomeError::Forbidden) }

I just find

Err(SomeError::Forbidden).unless_err(!predicate)?;

to be a bit more readable than those alternatives, while also maintaining the 'one line' / 'lack of blocks' spirit of the original post.

1 Like

What about <bool> else { <diverging block> } syntax? That is:

let Ok(x) = some_thing else { return; };
2 < 5 else { panic!("math is broken"); };
some_value.check_some_condition() else { continue; };

It has right to left evaluation order and is consistent with the let-else.

You're right that let … else has a bit of that, but at least the pattern in let has to be fallible for else to work. Because of this you can know to expect a different kind of statement quite early in the line, before the arbitrary expression. And the diverging expression is last, not first, so the execution order is preserved.

In case of match, I don't find if as problematic, because match has a list of patterns, not statements. Patterns are expected to be not taken if they don't match, so I'm not surprised to see a line that is a pattern that may not do anything. OTOH regular statements (non-indented ones, that are not in an if block) are typically executed, so seeing a statement that looks like it's reached, but is not executed, is a surprise.

I would expect that to have left-to-right evaluation order, not right-to-left.

And in any case, that seems like a readability regression compared to if !cond { diverge }.

1 Like

I saw a lot of good points made here.

LR TB evaluation order:

Would it be better to use a new keyword then, allowing to preserve the order? I am thinking:

if !may_i_do_this { return Err(No); }

unless may_i_do_this return Err(No);

I see this code is even more similar to the if statement, but clearer. It also gives the option to either

  • Enforce diverging control flow like in a let-else or
  • Use it as an alternative to if instead - this might make inverted expressions obsolete but i personally do not like this as much as point one.

Result::unless_err(self, p: bool) For Results and maybe Options, this looks like a nice idea, though I'd change it slightly:

if !may_i_do_this { return Err(No); }

Err(No).unless(may_i_do_this)?;

I think the _err is a bit confusing regarding weather the predicate must be true or false for the method to return Ok(()). Otherwise i like this especially since it does not require compiler changes. It might even be implemented for all types deriving Error so it can be used for anything.

Macros

The macro suggested in the comment looks neat, though having a keyword after a comma feels strange. The ensure macro from anyhow looks like a really good solution though. I find it better to have normal syntax instead though, since the linting / error propagation in macros if oftentimes worse than in plain code - especially in GUI heavy editors like JetBrains CLion

This obviously isn't clearer than anything, but even though it would be possible, it is not the intention to promote stuff like that, or having a really really large expression before the if, like another commenter pointed out.

It is always possible to write really bad code, even in Rust where stuff like that:

let mut a = 1;
println!("{}", a + { a += 1; a });

Is evaluated correctly. You may take a guess what the result should be.

The syntax i suggested is specifically meant for expressions that would fit in a single line, of course it is up to the devs to use it properly but it is possible to write bad code in Brainfuck as well and that does not have a single keyword :smiley:

1 Like

Just a syntax nit: let … else and if require a block for the statement list to ensure that there's never ambiguity even if may_i_do_this is a complex expression:

unless may_i_do_this { return Err(No); }

would be in keeping with the other cases of keyword cond { block }.

Note that editors can learn to treat function-like macros specially - println!, write! and other macros are already reasonably common in Rust code, and editors need to handle them well as a result.

And in this case, the macro becomes very trivial for a GUI editor to handle if you don't need the anyhow tricks of converting a formatted string to an anyhow::Error:

macro_rules! ensure {
    ($cond:expr, $err:expr) => {
        if !$cond { return Err($err); }
    }
}

This is used as

ensure!(may_i_do_this, No::Forbidden);

and if it were commonly found, or in core/std, the editors would learn to do a good job of it.

Alternatively, you could get the same effect with:

fn ensure<E>(cond: bool, err: E) -> Result<(), E> {
    if cond { 
       Ok(())
    } else {
        Err(err)
    }
}

used as:

ensure(may_i_do_this, No)?;

Both of these would be good prototyping routes you could consider, and publish as a crate - if it gets traction, that's a strong argument towards pushing something of this form into core.

2 Likes

I believe one line ifs should be allowed by all tools. Even though three line ifs are reasonable in limited amount, they quickly become too much when used for all conditional returns.

One line ifs have its own problems, namely that { and } on the same line attracts too much attention. However, I consider this to be not as severe. One can learn to not put their attention on something, but once we have spent two lines of screen real estate, there is no getting it back, no matter how much experience you have.

Most syntax sugars don't carry their weight. That's why they're always opposed. But by the time we're getting the braceless if Pre-RFC for the hundredth time, we should recognize that this demand is not going away. If we don't adopt it now, we'd eventually be forced to adopt it, simply because of their popularity. Therefore, if we want to keep Rust from bloating up, we should at least seek a less bad substitute.

Result::unless is almost good enough, but unfortunately only works on Result<(), E> functions. Maybe it should return another type that convert into Result, so that you can ? it.

If that doesn't work, ensure is also an option. Note that the ensure! macro only evaluate $err when $cond is true, but the ensure function evaluates err unconditionally. A ensure_with function might be in order.

1 Like

We also get regular demand for the removal of semicolons and braces, despite goto fail. And we get regular demand for ways to disable the borrow checker.

I absolutely think that demand is a signal we should be paying attention to, but that doesn't make it an inevitability, especially if the perennial proposals don't have answers for the downsides/tradeoffs.

21 Likes

This is what I instantly thought of from the title.

OP should just start using this (let true = cond else).

Just as the comment you replied to says - this is a cute trick you probably shouldn't use.

I tried something similar in one project (let Cycle::Render = *&executor.state else { ... };) but it has one downside: If you're used to it you know "If i see an let Cycle.Render in a line, it asserts we are currently rendering". Revisiting the project after a couple month, i saw it and was like "what in hell did I do there?" . . . "Ah wait a minute that was that".

If you're not used to it and especially when just trying to find a bug (or otherwise just reading, no coding) it breaks you out of the state of just "taking it in" (and getting it) iykwim.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.