Learnings from Advent Of Code Day 1 as a Rust Newbie
Kunal Desai
Posted on December 10, 2022
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:
use std::collections::BinaryHeap;
use std::fs::File;
use std::path::Path;
/*
BufRead isn't explicitly mentioned anywhere in the file, why do we need it imported?
Additionally, if it's not included, then `lines` method throws an error
I see that this has something to do with traits...
Reading: https://doc.rust-lang.org/book/ch10-00-generics.html
- Traits are similar to interfaces in other languages, but there are some differences - according to Rust Book
- They can have default implementations
- Traits can be parameters (any object that implement's that trait can be passed as a paramter)
BufReader implements the BufRead trait:
- https://doc.rust-lang.org/std/io/struct.BufReader.html#impl-BufReader%3CR%3E
- https://doc.rust-lang.org/src/std/io/buffered/bufreader.rs.html#55-96
Why do traits need to be imported in Rust?
- https://stackoverflow.com/questions/25273816/why-do-i-need-to-import-a-trait-to-use-the-methods-it-defines-for-a-type
*/
use std::io::{BufRead, BufReader};
fn main() -> Result<(), std::io::Error> {
/*
What is the '?' operator in Rust?
- https://stackoverflow.com/questions/42917566/what-is-this-question-mark-operator-about
- https://www.becomebetterprogrammer.com/rust-question-mark-operator/#:~:text=operator%20in%20Rust%20is%20used,or%20Option%20in%20a%20function.
*/
/*
Related: Exceptions in Rust
https://doc.rust-lang.org/book/ch09-00-error-handling.html
Two types of errors: recoverable and unrecoverable.
- Recoverable errors are where you just want to report the error to the user and retry the operation
- Unrecoverable errors are things like accessing beyond the end of an arraya and you immediately want to stop the program
For recoverable errors, there is type Result<T, E> and panic! macro for non-recoverable errors
Result is a type that represents either success or failure: https://doc.rust-lang.org/std/result/enum.Result.html#:~:text=Result%20is%20a%20type%20that,the%20module%20documentation%20for%20details.
T contains the success type and E contains the Error type.
enum Result<T, E> {
Ok(T),
Err(E),
}
When handling the Result<T, E> return type, it's common to use the match keyword like so:
match result {
Ok(success) => success,
Err(error) => panic!(error),
}
For unrecoverable errors, there is type panic!. You can either call this explicitly in the code, or by doing something bad like accessing the end of an array.
You can have Rust show a stack trace of the panic by setting an environment variable RUST_BACKTRACE = 1
*/
let path = Path::new("day1.txt");
let file = File::open(&path)?;
let reader = BufReader::new(file);
let mut curr_sum = 0;
let mut heap = BinaryHeap::<i32>::new();
/*
lines() is a function implemented by the BufRead trait. BufReader implements the BufRead trait.
*/
for line_result in reader.lines() {
/*
Calling a function on an object changes the object's ownership
Assignment leads to ownership and re-assignment also moves ownership, read here to understand why: https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#ways-variables-and-data-interact-move
However, if a value implements the Copy trait, then the value is copied
https://depth-first.com/articles/2020/01/27/rust-ownership-by-example/
String ownership examples: https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#the-string-type
*/
/*
References:
If you don't want to copy, but you also don't want to change ownership, you can borrow with the & character
If a function takes in a reference, the function doesn't own the value so it won't drop it upon completion of the function's execution
References are immutable by default, but adding the `mut` keyword makes it so that the reference is modifiable.
There is one big caveat which is that if you have a mutable reference to a value, you can't have any other references to that value.
https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
*/
let line = line_result?;
/*
Why can we call this function but still access `line` in the else statement?
Because .len takes in a string reference, so line is borrowed, not moved.
Ownership does not change as a result
*/
let is_new_line = line.len() == 0;
if is_new_line {
/*
What is the ! operator on println and why don't I need to import println via use?
*/
heap.push(curr_sum);
curr_sum = 0;
} else {
/*
What is unwrap? Give me the result and if there's an error, then panic. It's somewhat equivalent to:
match line {
Ok(line) => line,
Err(e) => panic!("Error"),
}
*/
curr_sum += line.parse::<i32>().unwrap();
}
}
const TOP_K_ELVES: i32 = 3;
let mut total_calories = 0;
for _ in 0..TOP_K_ELVES {
total_calories += heap.pop().unwrap();
}
println!(
"{:?} total calories were retrieved by the top {:?} elves",
total_calories, TOP_K_ELVES,
);
Ok(())
}
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.
Posted on December 10, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.