Advent of Code 2020 (this time in Rust)

  • This is a great way to learn a language!
    Not just the sequence of puzzles of increasing difficulty, but also that there are people proficient in the language solving the same problems. After day 3 I looked on r/adventofcode for people who had posted Rust solutions. I thought Axl Lind’s looked particularly clean and I learned a lot about idiomatic Rust from reading it. After that, my pattern was to solve the problem myself, then go look at how Axel solved it. This worked great, except for the one day he solved in C++ instead! I think next year I’ll use AoC as an opportunity to learn some Go.
  • Rust is an interesting language with lots of great ideas.
    I’d choose it over C++ for a project in its domain any day. But there are many things about it that are just annoying (see below). For work that doesn’t require me to work at such a low level, I’d rather use Python or TypeScript. And for writing command line programs, I suspect I’ll prefer Go.
  • This year’s Advent of Code was too easy.
    I kept waiting for it to get hard and it never did. I missed how last year’s puzzles built on one another (the IntCode computer appeared in many of them). This year’s were all independent. The global stats would seem to bear this out: over 12,000 people collected all 50 stars in 2020, whereas only ~3,000 did in 2019. There were ~50% more people who completed day 1 this year, so growth alone can’t account for the 4x increase.
  • Private leaderboards are fun but dangerous.
    Two of my coworkers did the Advent of Code this year and we set up a private leaderboard. This was good motivation to get the puzzles solved quickly. Being on the east coast made it hard to compete on the global leaderboard, particularly for later days, but I’m tempted to adjust my sleep schedule to do this next year!
  • Rust iterators are neat! And I hear they’re efficient, too. This was one of the main things I learned from reading Axel’s code. I started relying on them more and more over the course of this year’s AoC.
  • Rust editor support isn’t as good as I’m used to with TypeScript. Particularly around macros, some errors hiding other errors, {unknown} inferred types and the incredibly annoying "scroll through the Vec docs to see your error" issue. This made debugging chained method calls on iterators quite hard.
  • For all the talk about the borrow checker, it’s pretty rare that you actually use an explicit lifetime annotation. I only used one or two in the whole Advent of Code.
  • Understanding that String is for strings you own was helpful. (Otherwise you can use &str.) I often wound up resolving borrow checker errors by throwing in a String::from or .clone(). Not the most efficient, but certainly effective.
  • Rust never implicitly casts between numeric types. I understand the argument for this and like it in principle. But wow is it annoying in practice! Having to constantly convert back and forth to usize to index vectors got quite cumbersome. Particularly annoying is that you can't do x[i + (-1)] if i has type usize. Instead, you have to do x[((i as i32) + (-1)) as usize]. This came up a lot in problems where you could move in any direction on a grid. Is there a more idiomatic way to do this without all the casting?
  • Inference works a bit differently than in TS. It’s common for a type to be set based on later usage (e.g. a Vec). But return types for functions cannot be inferred. It's neat that you can write somthing like iter.collect::<Vec<_>>() to be explicit about the Vec but let the generic type be inferred. I missed being able to see inferred types midway through chains like you can in TypeScript (hover over a generic function to see which type parameters were inferred for it). The inability to infer return types of functions was particularly frustrating when the type I wanted to return couldn't be written down, e.g. because it involved a closure.
  • There are some forms of abstraction that the borrow checker makes either impossible or hard. For example, writing a function that consumes an iterator, filters & maps it, and returns another iterator. Does writing a function that processes an iterator depend on having garbage collection?
  • It’s worth learning all the methods on Options and Results since they come up so often. I wound up with unwrap() everywhere in my code. I wish you could use ? syntax with Option in addition to Result. Learning and_then was useful on Day 11.
  • Coming from TypeScript, I found it a bit surprising that individual variants of an Enum aren’t types. You can’t write let mask: Op::Mask = Op::Mask { ... };.
  • While it stopped being an issue after day 1, I still find the Rust project structure (with lib and bin) pretty baffling. I wanted my project to be mostly binaries with a few shared modules between them. After much flailing, I wound up with something that worked. But I'm still mystified as to when I have to write use aoc2020::util; (the package name) vs. being able to write use super::util; (as in rusty boggle)
  • For non-primitive types, it makes sense that functions like map and filter borrow the values over which they iterate. But since I was often working with Vec<i32>, the referencing and dereferencing felt like overkill. I also had a hard time figuring out when you could destructure borrows (e.g. write for &x in ...) and when you couldn't. The meaning of &&i32 isn't entirely clear to me. Should I think of references as being like pointers? Is this the same as C++, where a reference is just a non-nullable pointer?
  • I like how the error messages in Rust refer to GitHub issues and RFCs. For example, writing (dx, dy) = (-dy, dx) refers you to https://github.com/rust-lang/rfcs/issues/372. This makes the language development process feel very accessible.
  • There are a few missing constructs that feel like they should be built in, e.g. a HashMap or HashSet literal. I'm sure Rust will add these eventually. I eventually learned that you can write a macro to add this feature yourself. Pretty cool! I've never worked in a language that had macros like this before. They seem quite powerful.
  • Static values are quite limited in Rust. I’m not sure you can create a static Vec without lazy_static. I found it surprising that you have to write out array lengths for statics, for example.

Daily notes

Day 1: Report Repair

problem / solution

use aoc2020::util;
use crate::util;
use super::util;
use super::super::util;

Day 2: Password Philosophy

problem / solution

  1. .captures()[0] is the full match, and .captures()[1] is the first capture.
  2. The docs suggested using lazy_static! to initialize the RE once rather than in a loop, but this seemed to break type checking in VS Code.

Day 4: Passport Processing

problem / solution

Day 5: Binary Boarding

problem

(echo 'ibase=2'; cat inputs/day5.txt | perl -pe 's/B/1/g; s/F/0/g; s/R/1/g; s/L/0/g;' | sort) | bc | pbcopy

Day 6: Custom Customs

problem / solution

Day 8: Handheld Halting

problem / solution

struct Instruction {
op: String,
arg: i32,
}
enum Op {
Nop(i32),
Acc(i32),
Jmp(i32),
}

Day 9: Encoding Error

problem / solution

fn is_pair_sum(n: u64, nums: &[u64]) -> bool{
// Why can't I make the lambda look like: |(a, b)| a + b == n?
nums.iter().combinations(2).any(|x| x[0] + x[1] == n)
}
fn is_pair_sum(n: u64, nums: &[u64]) -> bool{
nums.iter().tuple_combinations().any(|(a, b)| a + b == n)
}
is_pair_sum(n, &nums[(i as i32 - preamble_len) as usize..i])
Time: 2961ms
Time: 7ms

Day 10: Adapter Array

problem / solution

let jolts: &[i32];
jolts.iter().map(|x| x);

Day 11: Seating System

problem / solution

  • There’s an unreachable!() macro you can use instead of panic!("reason"). Not exactly sure why you'd prefer this.
  • He used get and and_then to chain map lookups. I was looking for and_then! I tried using map_or but ran into some bugs that I couldn't figure out. I wound up rewriting this in a more imperative style.
  • He also wrote out an eight-element tuple of directions (DS). This was a source of confusing bugs for me, as I had a duplicate / missing entry in mine.
  • Axel tends to separate his as i64 from his as usize, the latter only appearing at the place where you index into an array.
  • Using |&&x| in a lambda is OK.
  • Apparently you can do this (i and j being parameters): let (mut i, mut j) = (i as i64, j as i64);
  • You can use use EnumType::* to drop the need to qualify its constituents.

Day 12: Rain Risk

problem / solution

  • (-90) % 360 = -90 in Rust
  • I wanted to write (dx, dy) = (-dy, dx) to rotate 90°, but was referred to https://github.com/rust-lang/rfcs/issues/372
  • Storing the waypoint delta, instead of its absolute position, wound up being a good choice.

Day 13: Shuttle Search

problem / solution

// primes: [(59, 4), (31, 6), (19, 7), (13, 1), (7, 0)]
n = 2093560 (mod 3162341)

Day 14: Docking Data

problem / solution

let mask: Op = ...;
let mask: Op::Mask = Op::Mask { ... };
Op::Mask { ones, zeros: _, xs }

Day 15: Rambunctious Recitation

problem / solution

$ cargo run --release --bin day15 0,20,7,16,1,18,15 30000000
Compiling aoc2020 v0.1.0 (/Users/danvk/github/aoc2020)
Finished release [optimized] target(s) in 0.52s
Running `target/release/day15 0,20,7,16,1,18,15 30000000`
nums: [0, 20, 7, 16, 1, 18, 15]
last spoken: 129262 after 30000000 rounds (2317 ms)
$ time python3 py/day15.py 0,20,7,16,1,18,15 30000000
After 30000000, last_spoken=129262
python3 py/day15.py 0,20,7,16,1,18,15 30000000 13.51s user 0.18s system 99% cpu 13.721 total

Day 17: Conway Cubes

problem / solution

lazy_static! {
static ref DS: Vec<(i32, i32, i32, i32)> = {
let mut v: Vec<(i32, i32, i32, i32)> = vec![];
for dx in -1..=1 {
for dy in -1..=1 {
for dz in -1..=1 {
if dx != 0 || dy != 0 || dz != 0 {
v.push((dx, dy, dz, dw));
}
}
}
}
assert_eq!(26, v.len());
v
};
}

Day 18: Operation Order

problem / solution

I wish that type weren’t “{unknown}”!

Day 19: Monster Messages

problem / solution

fn match_str<'a>(&self, txt: &'a str, rules: &HashMap<i32, Rule>) -> Option<&'a str> {
  • The borrow checker error that led to the signature above didn’t show up in VS Code, even after restarting the Rust server. It only showed up as an error when I ran the program.
  • I’m getting quite annoyed at errors showing up after extremely long documentation strings. You have to scroll all the way down through several pages of text to see the error. And if you scroll even a pixel too far, the whole dialog goes away.

Day 20: Jurassic Jigsaw

problem / solution

  • Wrote macros for map! and set! literals in util.rs. For some reason these are aoc2020::map and not aoc2020::util::map.
  • It’s confusing to me when you can do for &x in ... and when you can't.
  • .collecting an iterator of pairs into a hash map is a pretty neat pattern.
  • I think “Missing lifetime annotation” errors don’t show up in VS Code and prevent any other errors from showing up, either.

Day 23: Crab Cups

problem / solution

Day 24: Lobby Layout

problem / solution

Day 25: Combo Breaker

problem / solution

  • Day 13: bus arrival times
  • Day 19: a/b message parsing
  • Day 20: solving a puzzle w/ rotation & flips
  • Day 23: crab cups

--

--

Software Developer @sidewalklabs, author of Effective TypeScript

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Dan Vanderkam

Dan Vanderkam

507 Followers

Software Developer @sidewalklabs, author of Effective TypeScript