I've always been curious to learn a little bit more about Rust and its properties, so I decided to take part in the Advent Of Code challenge and do it in Rust! In this post, I'm going to share my learnings about Rust as a newbie going through the first challenge in Advent Of Code. Here is the day1 challenge: https://adventofcode.com/2022/day/1. I'd encourage looking through it so that you can understand what the code below is intended to do.
First, here is the code that you can look over:
I've included comments in various sections describing some of the things I learned and was curious about.
Let's walk through the code, highlighting the interesting parts.
Main function
Line 24 declares the main function. This is the entry point to a Rust program. The keywords after ->
indicates the return value of the function. In this case, the return value is of type Result
. It looks like this:
enum Result<T, E> {
Ok(T),
Err(E),
}
This is a very common type in Rust that you'll see everywhere. I'll explain why in a following section.
In the Result
enum, Ok
implies the success response and Err
implies the Error type. In our case, the success return type is ()
which is known as the unit
primitive: https://doc.rust-lang.org/std/primitive.unit.html. It's similar to the void
type in other languages. The error type is of type std::io::Error
.
Reading A File
To solve the challenge, we must first read in a file of information. I downloaded the file and saved it in my repo under day1.txt
. The next step is to use Rust to read the file.
I use the std::path::Path
module in the Rust standard library to do this. To do that, I first reference the module as a use
declaration above to tell the compiler that I'm going to be using symbols from that module. The Path::new
function returns a reference to a path object that can be passed into File::open
for reading. I'll talk about references in a different post.
Next, I pass in the Path
reference to the File::open
function which, if you look at the docs, returns a std::io::Result
type. This type is just a shortcut for the std::result::Result
type that we saw earlier in the main
function - it's a very common return type in Rust.
? Operator
The interesting thing about this line of code is the ? mark operator at the end of the File::open
function call. What does it do?
The ?
operator is specific to Result
or Option
function return types. It causes the function to return with the error if there is any error in the File::open
call. If there isn't an error, in this case, it'll return the success type of the Result
enum which is a File
object.
When used on the Result
type, it operates similarly to the following code:
let file = match File::open(&path) {
Ok(file) => file,
Error(error) => return Error(),
}
This code returns a File
object if File::open
succeeds. If there is an error in it, then the main
function returns with the std::io::Error
that File::open
may throw. When we use the ?
operator, the function must return a Result
with that Error
type.
Reading Line-By-Line
Now that we have the plumbing to read the file, let's go through it line-by-line to solve the challenge. We will use the BufReader
struct
to do this. The struct
implements a trait called BufRead
and this trait has a method lines()
which we can use to iterate through each line of the file. A trait in Rust is similar to interfaces in other languages. The interesting thing is that we need to call use std::io::BufRead
to tell the compiler that we want to use this trait. Otherwise, it'll throw an error saying that the lines()
method does not exist on the struct BufReader
.
The reader.lines()
call returns a Result<String, std::io::Error>
type. To get the String
value out of the result, we can, just like we did previously, use the ?
operator. Note that the error type in Result<String, std::io::Error>
is the same as the error type when we used the ?
operator on the File::open
call which is why we can use the ?
operator here as well.
Now that we're able to read each line, we implement the logic to solve the challenge. I'm not going to talk too much about the logic as this post is intended to talk about Rust, not the challenge.
Converting a String to an i32
line.parse::<i32>()
is a function you can use to convert a String to an i32
. Its return type is Result<i32, ParseIntError>
. I initially tried to pull the i32
out of the Result
return type using the ?
operator, but the compiler wouldn't let me. The reason is because, when using the ?
operator, upon an error of the parse
method, the main
function would return with error ParseIntError
. Right now, the main
function is returning an error of type std::io::Error
which is a different type from ParseIntError
.
So, what is another way that we can pull the i32
out of the Result<i32, ParseIntError>
type? Similar to what we discussed earlier, we can use the match
keyword like so:
curr_sum += match line.parse::<i32>() {
Ok(val) => val,
Err(e) => panic!("Error trying to turn the string to an int"),
}
This snippet will return the i32
if it's able to convert the String
to an i32
. If not, it will panic!
. What is a panic
? A panic
is an unrecoverable error that will cause the program to exit immediatly.A short-hand of the above match
snippet is to use the unwrap
method.
Conclusion
These are some of the interesting things I learned about Rust in the first challenge in Advent Of Code. I also learned a bunch about ownership, references, and borrowing which I'll save for a different post since those are much longer topics.