blog(aoc-2022): add initro

Signed-off-by: Matej Focko <mfocko@redhat.com>
This commit is contained in:
Matej Focko 2022-12-14 21:39:32 +01:00
parent 5aeeb1142f
commit 9019b2809d
Signed by: mfocko
GPG key ID: 7C47D46246790496

364
blog/aoc-2022/00-intro.md Normal file
View file

@ -0,0 +1,364 @@
---
title: Advent of Code '22 in Rust
description: Preparing for Advent of Code '22.
date: 2022-12-14T21:45
slug: aoc-2022/intro
authors:
- name: Matej Focko
title: "a.k.a. @mf"
url: https://gitlab.com/mfocko
image_url: https://github.com/mfocko.png
tags:
- aoc-2022
- advent-of-code
- rust
hide_table_of_contents: false
---
Let's talk about the preparations for this year's [_Advent of Code_].
<!--truncate-->
## 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
```bash
# 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 Martin[^2].
```py
#!/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:
```yaml
# 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 binaries[^3]. 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:
```rust
/// 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.
```rust
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.
```rust
type Input = String;
type Output = i32;
```
:::tip
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.
```rust
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.
```rust
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).
```rust
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_.
:::caution
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:
```rust
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(())
}
```
[^1]: 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].
[^2]: [GitHub profile](https://github.com/martinjonas)
[^3]: Even though you can use it even for libraries, but handling errors from
libraries using `anyhow` is nasty… You will be the stinky one ;)
[_Advent of Code_]: https://adventofcode.com
[GitLab]: https://gitlab.com/mfocko/advent-of-code-2022
[Cargo]: https://doc.rust-lang.org/cargo/
[`rust-analyzer`]: https://rust-analyzer.github.io/
[`direnv`]: https://direnv.net/
[`tracing`]: https://crates.io/crates/tracing
[`tracing-subscriber`]: https://crates.io/crates/tracing-subscriber
[`thiserror`]: https://crates.io/crates/thiserror
[`anyhow`]: https://crates.io/crates/anyhow
[`color-eyre`]: https://crates.io/crates/color-eyre
[`regex`]: https://crates.io/crates/regex
[`lazy_static`]: https://crates.io/crates/lazy_static
[`itertools`]: https://crates.io/crates/itertools
[this Reddit post and the comment]: https://www.reddit.com/r/adventofcode/comments/zb98pn/comment/iyq0ono