Making a Path variable
Rust goes to great lengths to make sure that your program is cross-platform. One
of the ways it does this is by providing the Path module, which helps format
strings and string slices into OS-specific path types. The official Rust docs
recommend to use a Path variable instead of a string slice anytime you’re
working with filenames. This is one of several special tools from Rust that
comes from Rust’s std library–which, for the purposes of this tutorial, you
can think of as like a namespace.
In Rust, we can tell our program that we want to use Path objects by going to
the top of main.rs and adding:
| |
Now we can create a Path object from the argument passed to the
parse_markdown_file() function:
| |
The call to Path::new() creates a new Path object for us. Now, input_filename
is a Path that we can try to open. Again, don’t use a String object to
hold a filename, since the Path object is specificially designed to play well
with Rust’s other tools for opening, reading, and writing to files.
With our path now an official Rust Path variable, let’s pull in another tool
to be able to open the file that the path points to: File.
Just like when we created the Path variable, we will first need to declare that
we want to use it by adding the following to the top of main.rs:
| |
Notice that File comes from std::fs, whereas Path comes from std::path.
Back to our parsing function, after declaring the input_filename path variable,
let’s create a new File variable with it:
| |
Interesting! Now we have a semantic way to open a file using the File::open()
function, to which we pass a reference to input_filename, and then to which
we chain the .expect() function. You will encounter the .expect() function
a lot in your Rust development; it’s used to remove the verbosity around Rust’s
Result type.
In a nutshell, many functions in Rust do not just return a value, they return
a Result. A Result in Rust has two parts: Ok() and Err(). When you call
a function that returns a Result type, you generally need to check whether
the function was successful (and thus returned an Ok()) or not successful (and
thus returned an Err()). It’s akin to exception handling in other languages,
except here it’s baked into the function itself.
What the .expect() does is tell Rust to unwrap the return value and pass along
the Ok()—except upon failure, in which case the program quits and emits
a string ("Couldn't open file").
So when we write something like this:
| |
We’re basically writing a less verbose version of this:
| |
💭 Why the panic!() macro instead of println!()?
The panic!() macro respects the return type inferred from File::open(), which
is type std::fs::File. If you try to use println!() here instead, you will
get an error that says something like match arms have incompatible types. This
is because println!() doesn’t know what to do with a std::fs::File type, but
panic!() doesn’t care what kind of variable you are dealing with.
As you can see, the verbose way requires a lot more of a deep dive into Rust than this tutorial is designed for. For now, the condensed way is good enough to build confidence in using Rust; once confidence is there, you will have plenty of room to confuse yourself with Rust’s esoteric way of doing things– and ultimately become a stronger developer in all languages because of how Rust forces you to think!
There are a lot of new things in the verbose example above, but since you have
some experience interpreting match blocks in Rust already, try to see what’s
going on. Notice, for example, how we can assign a value to a variable based on the
result of a match block.
