mirror of
https://github.com/mfocko/blog.git
synced 2024-11-25 22:31:56 +01:00
blog(aoc-2022): add initro
Signed-off-by: Matej Focko <mfocko@redhat.com>
This commit is contained in:
parent
5aeeb1142f
commit
9019b2809d
1 changed files with 364 additions and 0 deletions
364
blog/aoc-2022/00-intro.md
Normal file
364
blog/aoc-2022/00-intro.md
Normal 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
|
Loading…
Reference in a new issue