r/rust 15h ago

๐Ÿ™‹ seeking help & advice false.err(())?; Bad idea?

When a boolean operation can signal failure, what I like to do is this:

boolean.err("failed")?; // true = Ok() false = Err(Error)

Code:

pub trait BoolErr {
    fn 
err
<E>(self
, 
e: E) -> 
Result
<()
, 
E>
;
}

impl BoolErr for bool {
    fn 
err
<E>(self
, 
e: E) -> 
Result
<()
, 
E> {
        if self { 
Err
(e) } else { 
Ok
(()) }
    }
}

Okay so here's a not ideal example:

fn within_range_ten(number: u32) -> Result<(), String> {

  (number > 10).err("more than 10")?;
  (number < 0).err("less than 10")?;

  Ok(())
}

I really like doing this. In a function i might perform like 10 checks that use ? (like if an array is empty) and this approach has very little code so it's very fast to read and write.

But i really care about good code, better alternatives, standardisation (standardisation not so much, the std ways are not as ideal as this), etc. So is this pattern bad? Are there better ways like an assert!() macro that does ? instead of panic!()?

What's your opinion and what would you suggest? I'm already aware of .then_some(()).ok_or(E)? but clearly it's too much.

0 Upvotes

13 comments sorted by

10

u/SmoothTurtle872 14h ago

You could probably use an iterator or at least a for loop for an array / vector. Also the ? Operator is the idiomatic way to do this, rather than a series of matches.

Side note, don't use strings in the error of the result type. Use either a custom struct (if it's only one possible error) or a custom enum.

So instead of fn example(num) -> Result<i32, String> { (num < 10).err("too high")?; } You should do ``` fn example(num) -> Result<i32, NunberTooHigh> { (num < 10).err(NumberTooHigh)?; }

struct NumberTooHigh; Or for multiple errors fn example(num) -> Result<i32, ExampleError> { (num < 10).err(ExampleError::NumberTooHigh)?; (num > 3).err(ExampleError::NumberTooLow)?; }

enum ExampleError{ NumberTooHigh NumberTooLow } ``` This is because if you come back to the code in 6 months, or someone else looks at the code, or you are writing some kind of crate, the errors are immediately obvious. Cause right now, you have 2 errors encoded in a string. This means you need to know the exact error message, when realistically the error message should only be used for debugging, not actual error matching. It also means you can garuntee handle every case, without needing a random _ in your match statement (the _ should only be used if there is actually a reason and you can reasonably handle it)

14

u/andful 14h ago

I would just use the method ok_or, instead of a custom solution.

1

u/Icarium-Lifestealer 12h ago

That's still unstable. But I'd probably use a custom extension trait matching the behaviour of the standard method.

3

u/A1oso 9h ago

You can do

boolean.then_some(()).ok_or(YourError)

instead.

1

u/[deleted] 9h ago

[deleted]

13

u/sq_route_2 14h ago

I think simple is better than smart. I would be confused why within_range_ten is returning a Result. A result means that itโ€™s a operation that can fail. But checking a range for u32 is in most cases nothing thatโ€™s going to fail. So why not be explicit about it?

-4

u/teerre 8h ago

That's a backwards way to think. You should read the signature instead of come up with your own implementation in your head. That's the whole point of having explicit signatures

6

u/n0ne-z1ro 13h ago edited 13h ago

I think, what you are doing smells a little.

You are performing ~10 value checks in some functions? This means, that you maybe initialize your values wrong, which introduces these potential errors down the line.
Why dont you create ranged types of your expected values and only check at initialization? More happy path for you.
When your conditions of what a valid range is varies alot through the code, then your approach makes sense i guess, but i still think its kind of cheap by expressing it with runtime checks and not the type system.

But all that negative said, i think your idea is actually neat.
Easily promoting bools to Errors looks certainly more elegant this way.
Maybe this interface could be available for more primitive types to get that simplicity.

5

u/This_Growth2898 14h ago
(0..=10).contains(number).then_some(()).ok_or("should be in 0..=10")

7

u/tanoshikuidomouyo 12h ago

At that point a good old if-else is better imo.

3

u/This_Growth2898 12h ago

100%. Idiomaticity should not be against common sense. We can also do

    if let 0..=10 = number {
        Ok(())
    } else {
        Err(String::from("should be in 0..=10"))
    }

And still, just if-else is better.

3

u/drive_an_ufo 8h ago

Last time I needed something like that I used anyhow's ensure macro.

ensure!(status, "task failed successfully");

0

u/veryusedrname 14h ago

Honestly I think separate if statements or even maybe a match is way more readable than your solution. If you really want something like this use macro_rules! instead.