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.

  1. We can use the ? operator to propagate errors to the calling function. E.g., if fs::read_from_string() was unable to read the file, read_int_from_file() will return an Err(err) where err is the boxed up error. This makes our code shorter and easier to read (with practice).

  2. The ? operator knows how to turn any Result<T, E> (where E is some type that implements std::error::Error, as all of the standard library errors do) into a Result<T, Box<dyn std::error::Error>>.

    So even though neither fs::read_from_file() nor str::parse() return the correct type of Result, ? will perform the conversion for us.

  3. The standard library knows how to turn a String into a Box<dyn std::error::Error>. This is incredibly helpful as the next tip shows.

Tip

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.