Learnings from Advent Of Code Day 1 as a Rust Newbie

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.

If you have any questions, feel free to reach out to me on Twitter (DMs open) or via email