Here's a somewhat long-winded example of using both map_err and ?, including the ownership/capturing problems that closures can introduce.
I do a lot of map_err mapping to add context:
struct Error { path: Box<Path>, kind: ErrorKind, }
enum ErrorKind {
Open(io::Error),
Read(io::Error),
Parse(ParseError),
}
impl<P: Into<Box<Path>>> From<(P, ErrorKind)> for Error { ... }
fn example(path: &Path) -> Result<(), Error> {
let file = File::open(path).map_err(|e| (path, ErrorKind::Open(e)))?;
do_stuff(file).map_err(|e| (path, e))?;
Ok(())
}
fn do_stuff(file: File) -> Result<(), ErrorKind> { ... }
Here we're using both map_err and ? to construct better errors. With I/O in particular, errors can arise many tiimes in a single function, and it can be quite cumbersome to have matches and struct construction expressions all over the place.
let file = match File::open(path) {
Ok(file) => file,
Err(e) => return Err(Error {
path: path.into(),
kind: ErrorKind::Open(e),
}),
};
Now, in this scenario, you can easily run into ownership conflicts:
fn other_example(mut path: PathBuf) -> Result<PathBuf, Error> {
// edit the path or whatever, then
let file = File::open(&*path).map_err(|e| (path, ErrorKind::Open(e)))?;
do_stuff(file).map_err(|e| (path, e))?;
Ok(path)
}
error[E0382]: use of moved value: `path`
--> src/lib.rs:35:28
|
33 | fn other_example(mut path: PathBuf) -> Result<PathBuf, Error> {
| -------- move occurs because `path` has type `PathBuf`, which does not implement the `Copy` trait
34 | let file = File::open(&*path).map_err(|e| (path, ErrorKind::Open(e)))?;
| --- ---- variable moved due to use in closure
| |
| value moved into closure here
35 | do_stuff(file).map_err(|e| (path, e))?;
| ^^^ ---- use occurs due to use in closure
| |
| value used here after move
You could just eat the cost of a clone here:
// the `From` impl will effectively clone the `PathBuf` now
do_stuff(file).map_err(|e| (&*path, e))?;
And I don't really mind this at all, as it is the error path. However, an alternative is to avoid the closure here and match.
fn other_example(path: PathBuf) -> Result<PathBuf, Error> {
let file = match File::open(&*path).map_err(ErrorKind::Open) {
Ok(file) => file,
Err(e) => return Err((path, e).into()),
};
match do_stuff(file) {
Ok(()) => Ok(path),
Err(e) => Err((path, e).into()),
}
}
And this comes up with map, or_else, and other similar closure-utilizing methods too, it's not an error handling specific situation.
Note how using some mapping (and sometimes ?), etc, can still avoid some of the verbosity.
Yes, closures can get inlined.
But note that the capturing of variables used to create the closure cannot be inlined away, in the sense that the ownership example above will start compiling. If creating and passing the closure somewhere is unconditional, so is the capturing.