Part 2: Result, again (5 points)
In the previous lab, you created a type alias Result<T>
that you used for returning multiple types of errors from a function. You’re
going to make extensive use of that type in this lab.
As a reminder, here’s a function that reads an integer from a file and returns it.
#![allow(unused)] fn main() { use std::fs; use std::path::Path; // A generic Result type that can hold any type of Error. type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>; fn read_int_from_file(path: &Path) -> Result<i32> { let num = fs::read_to_string(path)? .trim() .parse()?; Ok(num) } }
This function can fail in multiple ways including the file not existing or being inaccessible and the contents of the file not being an integer.
There are three key benefits of doing this.
-
We can use the
?
operator to propagate errors to the calling function. E.g., iffs::read_from_string()
was unable to read the file,read_int_from_file()
will return anErr(err)
whereerr
is the boxed up error. This makes our code shorter and easier to read (with practice). -
The
?
operator knows how to turn anyResult<T, E>
(whereE
is some type that implementsstd::error::Error
, as all of the standard library errors do) into aResult<T, Box<dyn std::error::Error>>
.So even though neither
fs::read_from_file()
norstr::parse()
return the correct type ofResult
,?
will perform the conversion for us. -
The standard library knows how to turn a
String
into aBox<dyn std::error::Error>
. This is incredibly helpful as the next tip shows.
When you are dealing with file input/output, we can use
Result::map_err()
to attach context to our errors.
map_err()
lets us convert from one error type to another. Here’s an example
of attaching the path of a file that failed to open to the error message.
use std::fs::File; use std::path::{Path, PathBuf}; type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>; fn bad_error_message(path: &Path) -> Result<()> { let mut f = File::open(path)?; todo!() } fn good_error_message(path: &Path) -> Result<()> { let mut f = File::open(path) .map_err(|err| format!("{}: {err}", path.display()))?; todo!() } fn main() { let path = PathBuf::from("/path/to/no-such-file"); println!("Calling the function with the bad error message:"); if let Err(err) = bad_error_message(&path) { // This should be eprintln!() println!("{err}"); } println!("\nCalling the function with the good error message:"); if let Err(err) = good_error_message(&path) { // This should be eprintln!() println!("{err}"); } }
Click the Run button and look at the two error messages. Notice that the good error message shows the path of the file that couldn’t be opened and the bad one does not.
Let me assure you there’s little that’s as frustrating as trying to debug code (in any language) where the error messages don’t tell you which files couldn’t be opened.
You are going to want to transform the errors, particularly file I/O errors, in this way to add context.
Your task
Start by deleting the contents of lib.rs
and create a public type alias
named Result<T>
which is an alias for a standard Result
whose error is
boxed just as you did in the previous lab.
In order to be able to use this type from outside the process
library, you
need to make the type public. To do that, add pub
before it.
pub type Result<T> = ...;
This is similar to the public
keyword in Java.
In runnable.rs
, add the line
use process::Result;
Now, any time you use Result
in runnable.rs
, it will use the Result
type you
defined in the library.
At this point, your runnable
binary is now broken because you’ve removed the add
function.
In Part 3, you will fix that by making runnable
read a file from
/proc
, parse it, and print out some information.