diff --git a/blog/aoc-2022/00-intro.md b/blog/aoc-2022/00-intro.md new file mode 100644 index 0000000..37ef59d --- /dev/null +++ b/blog/aoc-2022/00-intro.md @@ -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_]. + + + +## 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>(pathname: P) -> String; + +/// Reads file and returns it as a vector of characters. +pub fn file_to_chars>(pathname: P) -> Vec; + +/// 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, T: FromStr>(pathname: P) -> Vec +where + ::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( + iter: impl Iterator +) -> Vec +where + ::Err: std::fmt::Debug, + U: Deref; + +/// Reads file and returns it as a vector of its lines. +pub fn file_to_lines>(pathname: P) -> Vec; +``` + +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