Ian Jackson: Rust needs #[throws]
tl;dr:
edited 2022-12-18 19:58 UTC to improve, and 2022-12-18 23:28 to fix, formatting
comments
Ok-wrapping as needed in today s Rust is a significant distraction, because there are multiple ways to do it. They are all slightly awkward in different ways, so are least-bad in different situations. You must choose a way for every fallible function, and sometimes change a function from one pattern to another.
Rust really needs #[throws] as a first-class language feature. Code using #[throws] is simpler and clearer.
Please try out withoutboats s fehler. I think you will like it.
Contents
- A recent personal experience in coding style
- What is
Okwrapping? Intro to Rust error handling - A minor inconvenience, or a significant distraction?
- What is to be done, then?
- Appendix - examples showning code with
Okwrapping is worse than code using#[throws]
fehler, I have been using it in most of my personal projects.
For Reasons I recently had a go at eliminating the dependency on fehler from Hippotat. So, I made a branch, deleted the dependency and imports, and started on the whack-a-mole with the compiler errors.
After about a half hour of this, I was starting to feel queasy.
After an hour I had decided that basically everything I was doing was making the code worse. And, bizarrely, I kept having to make individual decisons about what idiom to use in each place. I couldn t face it any more.
After sleeping on the question I decided that Hippotat would be in Debian with fehler, or not at all. Happily the Debian Rust Team generously helped me out, so the answer is that fehler is now in Debian, so it s fine.
For me this experience, of trying to convert Rust-with-#[throws] to Rust-without-#[throws] brought the Ok wrapping problem into sharp focus.
What is Ok wrapping? Intro to Rust error handling
(You can skip this section if you re already a seasoned Rust programer.)
In Rust, fallibility is represented by functions that return Result<SuccessValue, Error>: this is a generic type, representing either whatever SuccessValue is (in the Ok variant of the data-bearing enum) or some Error (in the Err variant). For example, std::fs::read_to_string, which takes a filename and returns the contents of the named file, returns Result<String, std::io::Error>.
This is a nice and typesafe formulation of, and generalisation of, the traditional C practice, where a function indicates in its return value whether it succeeded, and errors are indicated with an error code.
Result is part of the standard library and there are convenient facilities for checking for errors, extracting successful results, and so on. In particular, Rust has the postfix ? operator, which, when applied to a Result, does one of two things: if the Result was Ok, it yields the inner successful value; if the Result was Err, it returns early from the current function, returning an Err in turn to the caller.
This means you can write things like this:
let input_data = std::fs::read_to_string(input_file)?;
and the error handling is pretty automatic. You get a compiler warning, or a type error, if you forget the ?, so you can t accidentally ignore errors.
But, there is a downside. When you are returning a successful outcome from your function, you must convert it into a Result. After all, your fallible function has return type Result<SuccessValue, Error>, which is a different type to SuccessValue. So, for example, inside std::fs::read_to_string, we see this:
let mut string = String::new();
file.read_to_string(&mut string)?;
Ok(string)
string has type String; fs::read_to_string must return Result<String, ..>, so at the end of the function we must return Ok(string). This applies to return statements, too: if you want an early successful return from a fallible function, you must write return Ok(whatever).
This is particularly annoying for functions that don t actually return a nontrivial value. Normally, when you write a function that doesn t return a value you don t write the return type. The compiler interprets this as syntactic sugar for -> (), ie, that the function returns (), the empty tuple, used in Rust as a dummy value in these kind of situations. A block ( ... ) whose last statement ends in a ; has type (). So, when you fall off the end of a function, the return value is (), without you having to write it. So you simply leave out the stuff in your program about the return value, and your function doesn t have one (i.e. it returns ()).
But, a function which either fails with an error, or completes successfuly without returning anything, has return type Result<(), Error>. At the end of such a function, you must explicitly provide the success value. After all, if you just fall off the end of a block, it means the block has value (), which is not of type Result<(), Error>. So the fallible function must end with Ok(()), as we see in the example for std::fs::read_to_string.
A minor inconvenience, or a significant distraction?
I think the need for Ok-wrapping on all success paths from fallible functions is generally regarded as just a minor inconvenience. Certainly the experienced Rust programmer gets very used to it. However, while trying to remove fehler s #[throws] from Hippotat, I noticed something that is evident in codebases using vanilla Rust (without fehler) but which goes un-remarked.
There are multiple ways to write the Ok-wrapping, and the different ways are appropriate in different situations.
See the following examples, all taken from a real codebase. (And it s not just me: I do all of these in different places, - when I don t have fehler available - but all these examples are from code written by others.)
Idioms for Ok-wrapping - a bestiary
Wrap just a returned variable binding
If you have the return value in a variable, you can write Ok(reval) at the end of the function, instead of retval.
pub fn take_until(&mut self, term: u8) -> Result<&'a [u8]>
// several lines of code
Ok(result)
If the returned value is not already bound to variable, making a function fallible might mean choosing to bind it to a variable.
Wrap a nontrivial return expression
Even if it s not just a variable, you can wrap the expression which computes the returned value. This is often done if the returned value is a struct literal:
fn take_from(r: &mut Reader<'_>) -> Result<Self>
// several lines of code
Ok(AuthChallenge challenge, methods )
Introduce Ok(()) at the end
For functions returning Result<()>, you can write Ok(()).
This is usual, but not ubiquitous, since sometimes you can omit it.
Wrap the whole body
If you don t have the return value in a variable, you can wrap the whole body of the function in Ok( ). Whether this is a good idea depends on how big and complex the body is.
fn from_str(s: &str) -> std::result::Result<Self, Self::Err>
Ok(match s
"Authority" => RelayFlags::AUTHORITY,
// many other branches
_ => RelayFlags::empty(),
)
Omit the wrap when calling fallible sub-functions
If your function wraps another function call of the same return and error type, you don t need to write the Ok at all. Instead, you can simply call the function and not apply ?.
You can do this even if your function selects between a number of different sub-functions to call:
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
if flags::unsafe_logging_enabled()
std::fmt::Display::fmt(&self.0, f)
else
self.0.display_redacted(f)
But this doesn t work if the returned error type isn t the same, but needs the autoconversion implied by the ? operator.
Convert a fallible sub-function error with Ok( ... ?)
If the final thing a function does is chain to another fallible function, but with a different error type, the error must be converted somehow. This can be done with ?.
fn try_from(v: i32) -> Result<Self, Error>
Ok(Percentage::new(v.try_into()?))
Convert a fallible sub-function error with .map_err
Or, rarely, people solve the same problem by converting explicitly with .map_err:
pub fn create_unbootstrapped(self) -> Result<TorClient<R>>
// several lines of code
TorClient::create_inner(
// several parameters
)
.map_err(ErrorDetail::into)
What is to be done, then?
The fehler library is in excellent taste and has the answer. With fehler:
- Whether a function is fallible, and what it s error type is, is specified in one place. It is not entangled with the main return value type, nor with the success return paths.
- So the success paths out of a function are not specially marked with error handling boilerplate. The end of function return value, and the expression after
return, are automatically wrapped up inOk. So the body of a fallible function is just like the body of an infallible one, except for places where error handling is actually involved. - Error returns occur through
?error chaining, and with a new explicit syntax for error return. - We usually talk about the error we are possibly returning, and avoid talking about
Resultunless we need to.
fehler provides:
- An attribute macro
#[throws(ErrorType)]to make a function fallible in this way. - A macro
throws!(error)for explicitly failing.
- One does not need to decide where to put the
Ok-wrapping, since it s automatic rather than explicitly written out. - Specifically, what idiom to adopt in the body (for example
write!(...)?;vswrite!(...)in a formatter) does not depend on whether the error needs converting, how complex the body is, and whether the final expression in the function is itself fallible. - Making an infallible function fallible involves only adding
#[throws]to its definition, and?to its call sites. One does not need to edit the body, or the return type. - Changing the error returned by a function to a suitably compatible different error type does not involve changing the function body.
- There is no need for a local
Resultalias shadowingstd::result::Result, which means that when one needs to speak ofResultexplciitly, the code is clearer.
- You can t write
#[throws]on a closure. - Sometimes you can get quite poor error messages if you have a sufficiently broken function body.
- Code inside a macro call isn t properly visible to
fehlerso sometimesreturnstatements inside macro calls are untreated. This will lead to a type error, so isn t a correctness hazard, but it can be nuisance if you like other syntax extensions egif_chain. #[must_use] #[throws(Error)] fn obtain() -> Thing;ought to mean thatThingmust be used, not theResult<Thing, Error>.
#[throws] is so much nicer a language than Rust-with-mandatory-Ok-wrapping, that these are minor inconveniences.
Please can we have #[throws] in the Rust language
This ought to be part of the language, not a macro library. In the compiler, it would be possible to get the all the corner cases right. It would make the feature available to everyone, and it would quickly become idiomatic Rust throughout the community.
It is evident from reading writings from the time, particularly those from withoutboats, that there were significant objections to automatic Ok-wrapping. It seems to have become quite political, and some folks burned out on the topic.
Perhaps, now, a couple of years later, we can revisit this area and solve this problem in the language itself ?
Explicitness
An argument I have seen made against automatic Ok-wrapping, and, in general, against any kind of useful language affordance, is that it makes things less explicit.
But this argument is fundamentally wrong for Ok-wrapping. Explicitness is not an unalloyed good. We humans have only limited attention. We need to focus that attention where it is actually needed. So explicitness is good in situtions where what is going on is unusual; or would otherwise be hard to read; or is tricky or error-prone. Generally: explicitness is good for things where we need to direct humans attention.
But Ok-wrapping is ubiquitous in fallible Rust code. The compiler mechanisms and type systems almost completely defend against mistakes. All but the most novice programmer knows what s going on, and the very novice programmer doesn t need to. Rust s error handling arrangments are designed specifically so that we can avoid worrying about fallibility unless necessary except for the Ok-wrapping. Explicitness about Ok-wrapping directs our attention away from whatever other things the code is doing: it is a distraction.
So, explicitness about Ok-wrapping is a bad thing.
Appendix - examples showning code with Ok wrapping is worse than code using #[throws]
Observe these diffs, from my abandoned attempt to remove the fehler dependency from Hippotat.
I have a type alias AE for the usual error type (AE stands for anyhow::Error). In the non-#[throws] code, I end up with a type alias AR<T> for Result<T, AE>, which I think is more opaque but at least that avoids typing out -> Result< , AE> a thousand times. Some people like to have a local Result alias, but that means that the standard Result has to be referred to as StdResult or std::result::Result.
With fehler and #[throws]
|
Vanilla Rust, Result<>, mandatory Ok-wrapping
|
|---|---|
|
|
|
| Return value clearer, error return less wordy: | |
impl Parseable for Secret
|
impl Parseable for Secret
|
#[throws(AE)]
|
|
fn parse(s: Option<&str>) -> Self
|
fn parse(s: Option<&str>) -> AR<Self>
|
let s = s.value()?;
|
let s = s.value()?;
|
if s.is_empty() throw!(anyhow!( secret value cannot be empty ))
|
if s.is_empty() return Err(anyhow!( secret value cannot be empty ))
|
Secret(s.into())
|
Ok(Secret(s.into()))
|
|
|
|
|
No need to wrap whole match statement in Ok( ):
| |
#[throws(AE)]
|
|
pub fn client<T>(&self, key: & static str, skl: SKL) -> T
|
pub fn client<T>(&self, key: & static str, skl: SKL) -> AR<T>
|
where T: Parseable + Default
|
where T: Parseable + Default
|
match self.end
|
Ok(match self.end
|
LinkEnd::Client => self.ordinary(key, skl)?,
|
LinkEnd::Client => self.ordinary(key, skl)?,
|
LinkEnd::Server => default(),
|
LinkEnd::Server => default(),
|
|
)
|
|
|
Return value and Ok(()) entirely replaced by #[throws]:
| |
impl Display for Loc
|
impl Display for Loc
|
#[throws(fmt::Error)]
|
|
fn fmt(&self, f: &mut fmt::Formatter)
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
|
write!(f, :? : , &self.file, self.lno)?;
|
write!(f, :? : , &self.file, self.lno)?;
|
if let Some(s) = &self.section
|
if let Some(s) = &self.section
|
write!(f, )?;
|
write!(f, )?;
|
|
|
|
|
|
Ok(())
|
|
|
Call to write! now looks the same as in more complex case shown above:
| |
impl Debug for Secret
|
impl Debug for Secret
|
#[throws(fmt::Error)]
|
|
fn fmt(&self, f: &mut fmt::Formatter)
|
fn fmt(&self, f: &mut fmt::Formatter)-> fmt::Result
|
write!(f, "Secret(***)")?;
|
write!(f, "Secret(***)")
|
|
|
|
|
Much tiresome return Ok() noise removed:
| |
impl FromStr for SectionName
|
impl FromStr for SectionName
|
type Err = AE;
|
type Err = AE;
|
#[throws(AE)]
|
|
fn from_str(s: &str) -> Self
|
fn from_str(s: &str) ->AR< Self>
|
match s
|
match s
|
COMMON => return SN::Common,
|
COMMON => return Ok(SN::Common),
|
LIMIT => return SN::GlobalLimit,
|
LIMIT => return Ok(SN::GlobalLimit),
|
_ =>
|
_ =>
|
;
|
;
|
if let Ok(n@ ServerName(_)) = s.parse() return SN::Server(n)
|
if let Ok(n@ ServerName(_)) = s.parse() return Ok(SN::Server(n))
|
if let Ok(n@ ClientName(_)) = s.parse() return SN::Client(n)
|
if let Ok(n@ ClientName(_)) = s.parse() return Ok(SN::Client(n))
|
|
|
if client == LIMIT return SN::ServerLimit(server)
|
if client == LIMIT return Ok(SN::ServerLimit(server))
|
let client = client.parse().context( client name in link section name )?;
|
let client = client.parse().context( client name in link section name )?;
|
SN::Link(LinkName server, client )
|
Ok(SN::Link(LinkName server, client ))
|
|
|
|
|
Every year or so, I revisit the current best practices for Python packaging.
Like each month, have a look at the work funded by
A discussion on the debian-project mailing list caught my attention to an
Italian word meaning something like would you be so kind to please go
somewhere else? , but in a more direct and vulgar manner. I then used
Holger Levsen: Welcome, David, thanks for taking the time to talk with us today. First, could you briefly tell me about yourself?
David: Sure! I m David A. Wheeler and
I work for the
Mate desktop is not at all touch friendly even though I tried to
scaled up to read and tap things on the screen. Wiki suggest xfce4 is
little more touch friendly. I started moving to xfce. Then again same
problem, stuck at boot loop.
This time it was with lightdm, I turned off
A second update to the still-new package
First up: what I'm covering here is probably not relevant for most people. That's ok! Different situations have different threat models, and if what I'm talking about here doesn't feel like you have to worry about it, that's great! Your life is easier as a result. But I have worked in situations where we had to care about some of the scenarios I'm going to describe here, and the technologies I'm going to talk about here solve a bunch of these problems.
Ever since the demise of
I had bought a Thinkpad E470 laptop back in 2018 which was lying unused for
quite some time. Recently when I wanted to use it, I found that the keyboard is
not working, especially some keys and after some time the laptop will hang in
Lenovo boot screen. I came back to Bangalore almost after 2 years from my
hometown (WFH due to Covid) and thought it was the right time to get my laptop
back to normal working state. After getting the keyboard replaced I noticed that
1TB HDD is no longer fast enough for my taste!. I've to admit I never thought I
would start disliking HDD so quickly thanks to modern SSD based work laptops. So
as a second upgrade I got the HDD removed from my laptop and got a 240G SSD.
Yeah I know its reduction from my original size but I intend to continue using
my old HDD via USB SATA enclosure as an external HDD which can house the extra
data which I need to save.
So now that I've a SSD I need to install Debian Unstable again on it and this is
where I tried something new. My colleague (name redacted on request) suggested
to me use GRML live CD and install Debian via debootstrap. And after giving a
thought I decided to try this out. Some reason for going ahead with this are
listed below
