[solved] What are the various Error Handling mechanisms in Rust?


#1

Hello,

I am a programmer at ArrayFire and we are in the process of writing a Rust wrapper for our GPU library. You can find the library code over here and the Rust wrapper code here. ArrayFire is based off an generic multi-dimensional object known as array. We are having trouble wrapping our head around Rust’s error handling mechanism. Let me explain it with an example of Pi computation using Monte Carlo method.

C++ stub to do the Pi estimation looks like below.

try {
    array x = randu(samples,f32);
    array y = randu(samples,f32);
    double pi_val = 4.0 * sum<float>(sqrt(x*x + y*y) < 1) / samples;
 } catch (af::exception& e) {
     printf("Exception thrown: %s\n", e.what());
     exit(255);
 }

Operations such as *,+ in the above equation are element-wise.

First couple of lines using the Rust wrapper looks as below.

 let samples = 20000000;
 let dims = Dim4::new(&[samples, 1, 1, 1]);
 let x = match af::randu(dims, Aftype::F32) {
          Ok(v) => v,
          Err(e) => panic!("Randu failed with code {:?}", e),
      };
 let y = match af::randu(dims, Aftype::F32) {
          Ok(v) => v,
          Err(e) => panic!("Randu failed with code {:?}", e),
      };
 let sq = match af::sqrt( &x * &x + &y * &y ) {
          Ok(v) => v,
          Err(e) => panic!("sqrt failed with code {:?}", e),
      };
 let comp = match af::le( &sq, 1) {
          Ok(v) => v,
          Err(e) => panic!("sqrt failed with code {:?}", e),
      };
 let mut div_res = match af::sum_all(&div_res) {
          Ok(v) => v,
          Err(e) => panic!("sqrt failed with code {:?}", e),
      };
let pi_val = 4.0 * div_res / samples;

Now the part where equations such as below come into question.

  4.0 * sum<float>(sqrt(x*x + y*y) < 1) / samples;

We currently have implemented std::ops for Array struct that allows us to write arithmetic operations on Array’s, however they look different compared to C++ (which is natural i suppose, correct me if i am wrong). For example

z = x*x+y*y // C++ 
let z = &x * &x + &y * &y; // Rust

How do we approach error handling in such cases as above, or for that matter as in a simple case as below

 let x = match af::randu(dims, Aftype::F32) {
          Ok(v) => v,
          Err(e) => panic!("Randu failed with code {:?}", e),
      };

that enables us to use terse cascaded calls as below

let x = af::cos(af::randu(dims, Aftype::F32));

instead of doing the following.

let x = af::cos(match af::randu(dims, Aftype::F32) {
          Ok(v) => v,
          Err(e) => panic!("Randu failed with code {:?}", e),
      });

Any suggestions are appreciated,
Thank you,
Pradeep.


#2

http://blog.burntsushi.net/rust-error-handling/ is a VERY comprehensive
treatment of errors in Rust. We want to move it into the docs, @burntsushi
and I just haven’t had the time to do it yet.

(I can’t comment on your specifics at the moment, maybe someone else can,
or I can later if nobody else answers)


#3

You could probably translate the above directly into this (It uses Result::map):

let samples = 20_000_000;
let dims = Dim4::new(&[samples, 1, 1, 1]);
// Return inner variable or panic
// Save refs in `x` and `y` so `&` doesn't need adding later.
let x = &af::randu(dims, Aftype::F32).unwrap();
let y = &af::randu(dims, Aftype::F32).unwrap();
let sq = af::sqrt( x*x + y*y )
// If `Ok`, do computation, otherwise return `Err` value
            .map(|x| af::le(x, 1))
            .map(|x| af::sum_all(x)
            .map(|x| 4.0*x/samples)
// Return inner value if `Ok`, otherwise panic. All `Err`s
// propagate through all `maps`.
            .unwrap();

Each unwrap() could of course be replaced with a match if you prefer that.


#4

Will look into the article, thank you!


#5

Regarding the maps, it’d be pretty sweet for things like this if there was a math macro such that it would automatically translate all different ops (maybe by precedence rules or something) into a sequence of such maps. So, these would be equivalent:

math!(4.0 * sum(sqrt(x*x + y*y) < 1) / samples).unwrap()

let sq = sqrt( x*x + y*y )
        .map(|x| x < 1)
        .map(|x| sum(x))
        .map(|x| 4.0*x/samples)
        .unwrap();

#6

@mdinger true, it would be nice to have such a macro. I have started playing with Rust only a few weeks back, once i have a good hold on how to write fancy macros, may be we will add such macro if it is possible.

Also, you are welcome to contribute to arrayfire_rust wrapper, it is still in progress work and we plan to have a 100% functional wrapper soon.


#7

Rethinking it though, it might be against how Rust is designed to do error handling. They don’t use try/catch blocks for a reason (whatever that reason may be). Hmm…I can see both ways.


#8

@mdinger I was more thinking along the lines of pushing expressions along stack(something of sort) and then do evaluation as we unwind the operations/expressions. But I have no clue as to whether this is possible using rust macros or not.


#9

I think it’d be possible but I’m not certain. @DanielKeep would probably know better than I (if he cares to answer). The bigger questions though are:

  1. Should it be done at all?
  2. If it should be done, shouldn’t it be in an external library? Seems like a fairly generic use case.
  3. Is it worth the extra effort?

#10

map didn’t work me when i tried to compile the code, however and_then gave me the behavior i wanted.


#11

Cool. Basically the same idea.


#12

The problem is that you can’t deconstruct syntax elements in a macro.

What you want is something like:

macro_rules! checked {
    ($($lhs:expr + $rhs:expr):expr) => {
        checked!($lhs).and_then(|lhs| lhs.checked_add(checked!($rhs)))
    };
    // ...
}

But this is, sadly, impossible right now.

(Outside of procedural macros, I mean. You could totally do it with those, but that would trap you on nightly for who-knows how long, so I don’t feel it counts as a valid solution.)

Edit: Also, every other significantly less-optimal, but functional alternative I can think of would just end up being unusably verbose due to hygiene or because the orphan rules won’t allow it.

I’m beginning to think Rust actually should get some sort of special support for this kind of code, if only because it’s so hatefully verbose and obtuse otherwise.


#13

I knew you’d know something useful about this! Thanks for stepping in. Maybe a better solution will come in the future when people aren’t busy with other important things (like impl specialization).


#14

The try! macro offers a nice way to handle errors. It requires you to put the calculation in a separate function, so your final code could end somewhat similar to the following.

fn estimate_pi(samples: u32) -> Result<Aftype::F32,_> {
    let dims = Dim4::new(&[samples, 1, 1, 1]);
    let x = try!(af::randu(dims, Aftype::F32));
    let y = try!(af::randu(dims, Aftype::F32));
    let sq = try!(af::sqrt( &x * &x + &y * &y ));
    let comp = af::le( &sq, 1);
    Ok(4.0 * comp.sum_all() / samples)   //Edited. Thanks to Ilogiq
}
     
let samples = 2000000;
let pi = match estimate_pi(samples) {
     Ok(v)  => v,
     Err(r) => panic!("estimate_pi encountered an error: {}", e)
}

Here I assumed that af::le cannot throw an exception, so the wrapper should not return a result. On the other hand af::sqrt (and randu ??) can throw exceptions, so the wrappers can return a Result that is handles by try!() (EDIT: Various fixes)

EDIT2: Actually you don’t have to use sqrt in this algorithm. You should get the same result by writing just write. let sq = &x * &x + &y * &y; but that is a detail.

Thank you for wrapping angelfire. This looks like a great library.


#15

Shouldn’t the last line be Ok(4.0 * ...)?


#16

Oops. You are correct


#17

@DanielKeep Understood, thanks a lot for your inputs. Will keep an eye out for the new updates in Rust language for these kind of things. But for now, it seems like we are stuck with the things as they are now.

It would be definitely nice to have support for this kind of code in Rust native, of course with all proper error handling. Hopefully, we will see it soon.

@nielsle Sure, we actually did try try!. It resolves the issue in case of pi estimation example. This kind of code flow might be helpful for simple examples, but writing actual applications using such a construct might be a bit tad repulsive (at least i personally didn’t like the way the code looked), I may be wrong too!

Thanks once again everyone.


#18

I guess you’ll get used to it. It is the price for explicit error handling (which rocks), and the verbosity overhead is fairly minimal (try! and parens).


#19

This is a dangerous attitude; it can lead to things like checked exceptions in Java, where lots of people end up hacking around it because it’s too much of a pain to deal with in practice.

The real trick of Rust is not being safe; it’s being safe and usable at the same time. Sacrifice too much of one for the other, and the whole thing’ll collapse in on itself.

Having some kind of lifting support could be an enormous help, keeping most of the important explicitness (at interface boundaries), whilst making it less of an embuggerance in practice.


#20

That’s not what I meant. I wasn’t advocating unwrap(), which is kind of equivalent of blanket catch { } clauses in exception languages.

I simply meant that if using try! looks strange to you at the beginning, you will probably adjust to it when using the language for a longer time and it will feel natural. Just like indentation based syntax in Python.