Dec. 19th, 2016

jack: (Default)
I am still mulling this over after reading some articles on it (thanks, fanf, Kaela).

Background

Imagine you have a fairly simple function.

RetType func1(arg1, arg2)
{
   return func3(func2(arg1),func2(arg2)).func4();
}


But those other functions may encounter errors. Eg. they involve opening files, which may not be there.

Assume the error return can't usually be passed to a follow-up function.[1] The obvious then necessary step is for each function call, test the return value, if it's an error, return an error from this function. Else continue with the calculation. But this usually involves several lines of code for each of these functions, which obscures the desired control flow.

If you are willing to accept exceptions, you can just write the code above an allow any exceptions to propagate. But that represents a lot of hidden complexity from not knowing what might be thrown. And often overhead in runtime.

And in fact, this may obscure a common pattern, that for some function (eg. "parse this"), you SOMETIMES want to treat the failure as an error, and sometimes to interrogate it. As in, choose in the calling code whether failure is an error-value or exception.

Also remember, in C-like languages, many values unavoidably have a possible error case which can't be passed to other functions, null pointer. Ideally it would be clear which pointers might be null and which have already been assumed not to be.

In Rust

In Rust (if I understand correctly), these possibilities are often wrapped up in a try macro.

There is a conventional "Result" return type from most functions which may succeed or fail, which has one of two values. Either 'Ok' (usually though not required wrapping a return value). Or 'Err', wrapping a specific error (just a string, or an error object).

The try macro combines the "test return value, if it's an error, return that error from this function, else, evaluate to the successful value" into a brief expression:

try!(func2(arg))

Which seems like often what you want. Obviously if you want to handle the error in some way (say, you're interested in whether it succeeds, not just the successful result), you can interrogate the result value for ok or err.

And there's also a macro for "assume success, unwrap the result value, panic if it's not there", just like you can access a pointer without checking for null if you want. But functions which can't return an error shouldn't return "Result", so if you do that, it's clear you *might* fail. Which is exactly what you want for throw-away code. But it does mean, you can search for the unwrap macro if you want to find all the points where you did that and fix them.

Rust recent innovation: ?

I mention try! for historical reasons, but just recently, Rust has promoted it into a language feature, reducing the overhead further from four to six characters, to 1: '?' after a value means the same thing as the try macro.

Result<int, errtype="ErrType"> func1(arg1, arg2)
{
   return func3(func2(arg1)?,func2(arg2)?)?.func4()?; // Pseudocode, not actual rust syntax
}


Rust recent innovation: chaining

This is also really new and not standard yet, but I like the idea. Error chaining. The function .error_chain(|| "New error") is applied to the result of a function call. If it was a success, that's fine. If not, this error is added to the previous error. It is typically then followed by the try macro or ?. (I think?)

That means that your function can return a more useful error, eg. "could not open log file" or "could not calculate proportion". Which carries along the additional information of WHY it couldn't, eg. "could not open file XXXX in read mode" or "div by zero".

And then a higher level function can decide which of those it cares about handling -- usually not the lowest level one.

In some ways like exceptions, but (hopefully, because Rust) with no runtime overhead.

Footnotes

[1] I often think of it as, an error-value is one that, under any future operation of any sort, stays the same error value, but that's usually not how it's actually implemented.

Active Recent Entries