mirror of
https://github.com/mfocko/blog.git
synced 2024-12-22 09:41:22 +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