Let's talk about the preparations for this year's Advent of Code.
Choosing a language
When choosing a language for AoC, you usually want a language that gives you a quick feedback which allows you to iterate quickly to the solution of the puzzle. One of the most common choices is Python, many people also use JavaScript or Ruby.
Given the competitive nature of the AoC and popularity among competitive programming, C++ might be also a very good choice. Only if you are familiar with it, I guess…
If you want a challenge, you might also choose to rotate the languages each day. Though I prefer to use only one language.
For this year I have been deciding between Rust, C++ and Pascal or Ada.
I have tried Rust last year and have survived with it for 3 days and then gave up and switched to Kotlin, which was pretty good given it is “Java undercover”. I pretty much like the ideas behind Rust, I am not sure about the whole cult and implementation of those ideas though. After some years with C/C++, I would say that Rust feels too safe for my taste and tries to “punish me” even for the most trivial things.
C++ is a very robust, but also comes with a wide variety of options providing you
the ability to shoot yourself in the leg. I have tried to solve few days of previous
Advent of Code events, it was relatively easy to solve the problems in C++, given
that I do not admit writing my own iterator for enumerate
…
Pascal or Ada were meme choices :) Ada is heavily inspired by Pascal and has a pretty nice standard library that offers enough to be able to quickly solve some problems in it. However the toolkit is questionable :/
Choosing libraries
Preparations for Rust
All of the sources, later on including solutions, can be found at my GitLab.
Toolkit
Since we are using Rust, we are going to use a Cargo and more than likely VSCode
with rust-analyzer
. Because of my choice of libraries, we will also introduce
a .envrc
file that can be used by direnv
, which allows you to set specific
environment variables when you enter a directory. In our case, we will use
# to show nice backtrace when using the color-eyre
export RUST_BACKTRACE=1
# to catch logs generated by tracing
export RUST_LOG=trace
And for the one of the most obnoxious things ever, we will use a script to download the inputs instead of “clicking, opening and copying to a file”1. There is no need to be fancy, so we will adjust Python script by Martin2.
#!/usr/bin/env python3
import datetime
import yaml
import requests
import sys
def load_config():
with open("env.yaml", "r") as f:
js = yaml.load(f, Loader=yaml.Loader)
return js["session"], js["year"]
def get_input(session, year, day):
return requests.get(
f"https://adventofcode.com/{year}/day/{day}/input",
cookies={"session": session},
headers={
"User-Agent": "{repo} by {mail}".format(
repo="gitlab.com/mfocko/advent-of-code-2022",
mail="me@mfocko.xyz",
)
},
).content.decode("utf-8")
def main():
day = datetime.datetime.now().day
if len(sys.argv) == 2:
day = sys.argv[1]
session, year = load_config()
problem_input = get_input(session, year, day)
with open(f"./inputs/day{day:>02}.txt", "w") as f:
f.write(problem_input)
if __name__ == "__main__":
main()
If the script is called without any arguments, it will deduce the day from the system, so we do not need to change the day every morning. It also requires a configuration file:
# env.yaml
session: ‹your session cookie›
year: 2022
Libraries
Looking at the list of the libraries, I have chosen “a lot” of them. Let's walk through each of them.
tracing
and tracing-subscriber
are the crates that can be used for tracing
and logging of your Rust programs, there are also other crates that can help you
with providing backtrace to the Sentry in case you have deployed your application
somewhere and you want to watch over it. In our use case we will just utilize the
macros for debugging in the terminal.
thiserror
, anyhow
and color-eyre
are used for error reporting.
thiserror
is a very good choice for libraries, cause it extends the Error
from the std
and allows you to create more convenient error types. Next is
anyhow
which kinda builds on top of the thiserror
and provides you with simpler
error handling in binaries3. And finally we have color-eyre
which, as I found
out later, is a colorful (wink wink) extension of eyre
which is fork of anyhow
while supporting customized reports.
In the end I have decided to remove thiserror
and anyhow
, since first one is
suitable for libraries and the latter was basically fully replaced by {color-,}eyre
.
regex
and lazy_static
are a very good and also, I hope, self-explanatory
combination. lazy_static
allows you to have static variables that must be initialized
during runtime.
itertools
provides some nice extensions to the iterators from the std
.
My own “library”
When creating the crate for this year's Advent of Code, I have chosen a library type. Even though standard library is huge, some things might not be included and also we can follow KISS. I have 2 modules that my “library” exports, one for parsing and one for 2D vector (that gets used quite often during Advent of Code).
Key part is, of course, processing the input and my library exports following functions that get used a lot:
/// Reads file to the string.
pub fn file_to_string<P: AsRef<Path>>(pathname: P) -> String;
/// Reads file and returns it as a vector of characters.
pub fn file_to_chars<P: AsRef<Path>>(pathname: P) -> Vec<char>;
/// Reads file and returns a vector of parsed structures. Expects each structure
/// on its own line in the file. And `T` needs to implement `FromStr` trait.
pub fn file_to_structs<P: AsRef<Path>, T: FromStr>(pathname: P) -> Vec<T>
where
<T as FromStr>::Err: Debug;
/// Converts iterator over strings to a vector of parsed structures. `T` needs
/// to implement `FromStr` trait and its error must derive `Debug` trait.
pub fn strings_to_structs<T: FromStr, U>(
iter: impl Iterator<Item = U>
) -> Vec<T>
where
<T as std::str::FromStr>::Err: std::fmt::Debug,
U: Deref<Target = str>;
/// Reads file and returns it as a vector of its lines.
pub fn file_to_lines<P: AsRef<Path>>(pathname: P) -> Vec<String>;
As for the vector, I went with a rather simple implementation that allows only
addition of the vectors for now and accessing the elements via functions x()
and y()
. Also the vector is generic, so we can use it with any numeric type we
need.
Skeleton
We can also prepare a template to quickly bootstrap each of the days. We know that each puzzle has 2 parts, which means that we can start with 2 functions that will solve them.
fn part1(input: &Input) -> Output {
todo!()
}
fn part2(input: &Input) -> Output {
todo!()
}
Both functions take reference to the input and return some output (in majority
of puzzles, it is the same type). todo!()
can be used as a nice placeholder,
it also causes a panic when reached and we could also provide some string with
an explanation, e.g. todo!("part 1")
. We have not given functions a specific
type and to avoid as much copy-paste as possible, we will introduce type aliases.
type Input = String;
type Output = i32;
This allows us to quickly adjust the types only in one place without the need to do regex-replace or replace them manually.
For each day we get a personalized input that is provided as a text file. Almost all the time, we would like to get some structured type out of that input, and therefore it makes sense to introduce a new function that will provide the parsing of the input.
fn parse_input(path: &str) -> Input {
todo!()
}
This “parser” will take a path to the file, just in case we would like to run the sample instead of input.
OK, so now we can write a main
function that will take all of the pieces and
run them.
fn main() {
let input = parse_input("inputs/dayXX.txt");
println!("Part 1: {}", part_1(&input));
println!("Part 2: {}", part_2(&input));
}
This would definitely do :) But we have installed a few libraries and we want to
use them. In this part we are going to utilize tracing
(for tracing, duh…)
and color-eyre
(for better error reporting, e.g. from parsing).
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_target(false)
.with_file(true)
.with_line_number(true)
.without_time()
.compact()
.init();
color_eyre::install()?;
let input = parse_input("inputs/dayXX.txt");
info!("Part 1: {}", part_1(&input));
info!("Part 2: {}", part_2(&input));
Ok(())
}
The first statement will set up tracing and configure it to print out the logs to terminal, based on the environment variable. We also change the formatting a bit, since we do not need all the fancy features of the logger. Pure initialization would get us logs like this:
2022-12-11T19:53:19.975343Z INFO day01: Part 1: 0
However after running that command, we will get the following:
INFO src/bin/day01.rs:35: Part 1: 0
And the color_eyre::install()?
is quite straightforward. We just initialize the
error reporting by color eyre.
Notice that we had to add Ok(())
to the end of the function and adjust the
return type of the main
to Result<()>
. It is caused by the color eyre that
can be installed only once and therefore it can fail, that is how we got the ?
at the end of the ::install
which unwraps the »result« of the installation.
Overall we will get to a template like this:
use aoc_2022::*;
use color_eyre::eyre::Result;
use tracing::info;
use tracing_subscriber::EnvFilter;
type Input = String;
type Output = i32;
fn parse_input(path: &str) -> Input {
todo!()
}
fn part1(input: &Input) -> Output {
todo!()
}
fn part2(input: &Input) -> Output {
todo!()
}
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_target(false)
.with_file(true)
.with_line_number(true)
.without_time()
.compact()
.init();
color_eyre::install()?;
let input = parse_input("inputs/dayXX.txt");
info!("Part 1: {}", part_1(&input));
info!("Part 2: {}", part_2(&input));
Ok(())
}
Footnotes
-
Copy-pasting might be a relaxing thing to do, but you can also discover nasty stuff about your PC. See this Reddit post and the comment. ↩
-
Even though you can use it even for libraries, but handling errors from libraries using
anyhow
is nasty… You will be the stinky one ;) ↩