Advent of Rust

I have a bit of time off and I decided to participate in Advent of Code 2020 after my coworker Adrián shared a link to it. I’ve heard that people use challenges like these as an excuse to learn a new programming language, and I have wanted to learn Rust for quite a long time now.

Why Rust? From what I’ve heard, it’s a programming language oriented towards high performance and systems programming, like C or C++; but unlike those languages, it was designed in such a way to make it difficult or impossible to make mistakes such as buffer overflows or memory leaks. I have heard that it’s a lot more enjoyable to use than C++ as well.

I did write a “Hello World” program in Rust some time ago, and I have heard things about Rust from others, so I wouldn’t be coming to it completely fresh. Nonetheless, fresh enough that I decided that the experience of writing something from scratch, in a new programming language, was unusual enough for me that I would keep a log while I was doing it.

So here is that log. It’s a bit stream-of-consciousness. I’ve edited it so that it consists of complete sentences instead of just notes, but I’ve left in the mistakes and dead ends. I made this mainly so that I can look back later when I’ve learned more, and see what mistakes I made.

The reason I’m actually writing this log with all my mistakes in public though, is because I often see things on Twitter like:

RT if you are a senior developer who still Googles to remember syntax stuff

I think sharing facts like those is an important part of busting the myth of the “10x developer”. Developers google things all the time! Just to drive that point home, I thought it would be interesting to share just how much I googled during this afternoon of trying to learn a new programming language.1

At the same time, as you gain experience, you begin to see patterns, you start to know which search results are likely to help you; and other little things like, even if you don’t know what an error message means, you start being able to form a vague idea of where to look. I found that I drew on a lot of this sort of knowledge while trying to find my way around Rust as a newbie, and I’ve tried to note those thoughts down in the log. For example, “oh, this looks like this other thing I’m familiar with, so I’ll try such-and-such.”

I also really appreciate posts by Julia Evans where she chronicles her attempt to learn something new, and those are also an inspiration for turning this log into a blog post.

Day 1, Part 1

Each day’s puzzle is divided into two parts, and you get the second part once you complete the first part. The first part of the first day’s puzzle is to take a file, named input, with a list of numbers, one on each line, and find two numbers in that file that add up to 2020. The answer to the puzzle is what you get when you multiply those two numbers together.

First of all, I download the input file and put it in a directory: advent2020/1.1 (1.1 means Day 1, Part 1). To get started, let’s see if I can even write a Rust program to read the input file, get an array of numbers, and print it out. If I can get that first without bothering with the puzzle, then I’ll be able to start thinking about solving the puzzle afterward.

I google “rust program getting started” and land here, which looks like part of the official Rust documentation. The first thing to do is install Rust. I already had it installed because of trying to write a Hello World at some point in the past, so I skip the first few lines, and do rustup update to get the latest version. This downloads a bunch of stuff.

The Geting Started page says to use Cargo to set up my program. I know roughly what Cargo is, from having read or heard about it somewhere: a package manager. I wonder if a package manager is really necessary for tiny programs like this one, but I’ll go with the flow so that I can just follow the instructions.

The instructions do have a picture of a directory structure that Cargo will create when you run cargo new, and the src/ directory looks a bit inconvenient for a one-file program like I’m planning to write. Let’s see if I can get Cargo to set up my program so that it doesn’t put the source code in a src/ subdirectory. I do cargo new --help — for some reason, that’s not the real help, and it tells me to run cargo help new instead, so I do that.

OK, from the help I learn that Cargo will create a Git repository if there isn’t one already, and I certainly don’t want each puzzle to be in its own Git repository, so instead I initialize one in the advent2020/ directory. I do git init and git checkout -b main.

Also reading the help, I learn that Cargo will create a project directory, so I move the input file elsewhere, and remove the 1.1 directory that I already created, so that I can recreate it with Cargo. I don’t learn how to prevent creating a src/ directory, so I just leave that for now and go with the flow.

I have some trouble creating the project, because it’s not allowed to be named 1.1:

$ cargo new 1.1
error: the name `1.1` cannot be used as a crate name, the name cannot start with a digit
If you need a crate name to not match the directory name, consider using --name flag.
$ cargo new puzzle1.1
error: invalid character `.` in crate name: `puzzle1.1`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters)
If you need a crate name to not match the directory name, consider using --name flag.
$ cargo new puzzle1-1
     Created binary (application) `puzzle1-1` package

Hmph. I do know roughly what a crate is for the same reason I know roughly what Cargo is, but if I didn’t, this would be total gibberish to me. Anyway, my project is now named puzzle1-1, I guess.

Finally, I do cd puzzle1-1 and cargo run. The Hello World program that Cargo automatically generated, works! I can compile and execute Rust code, so now I can actually start writing it.

It’s time to find out how to read the input file. I copy it back into the puzzle1-1 directory and google “rust read file”. I land on another part of the Rust documentation, helpfully called “Reading a File”. (The last rust-lang.org result that I clicked on was good, so I’ll click on another one.)

I adapt the code given there, and end up with this:

use std::fs;

fn main() {
    let contents = fs::read_to_string("input").expect("Error reading file");
    println!("File contents:\n{}", contents);
}

As an aside, I dimly recall that Cargo has some sort of tool to automatically reformat your source code. I try typing cargo format and I get an error, but it also suggested to try cargo fmt, so I was close enough! That’s one win in favour of using Cargo, because now I don’t have to worry about formatting.

Anyway, I do cargo run and it builds and runs the program, and prints out the contents of the file. I see all the numbers!

So this is something, but it’s probably not correct. It gives me the whole file contents as a string, which I print out. I need the numbers, not one big string! What I actually want is to read each line of the file one at a time, and convert it to an integer, so that I end up with an array of integers, which I can use to solve the puzzle. How do I do that?

I google “rust read file line by line”, and the most likely-looking result is another Rust documentation page. This one is called “Rust by Example”. That sounds useful, examples are exactly what I want!

Once again I adapt the example code, and I end up with this:

use std::fs;
use std::io;
use std::path;

fn main() {
    if let Ok(lines) = read_lines("input") {
        for line in lines {
            if let Ok(ip) = line {
                println!("{}", ip);
            }
        }
    }
}

fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<fs::File>>>
where
    P: AsRef<path::Path>,
{
    let file = fs::File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}

This doesn’t compile:

error[E0599]: no method named `lines` found for struct `BufReader<File>` in the current scope
    --> src/main.rs:18:33
     |
18   |     Ok(io::BufReader::new(file).lines())
     |                                 ^^^^^ method not found in `BufReader<File>`
     |
     = help: items from traits can only be used if the trait is in scope
help: the following trait is implemented but not in scope; perhaps add a `use` for it:
     |
1    | use std::io::BufRead;
     |

At first I wonder if my version of Rust is different from the one used in the documentation? But from reading the error message, I also get a vague idea that my use std::whatever; statements that import the standard library modules must be wrong, because the error message is suggesting to add use std::io::BufRead;. That’s the main thing that I did differently than the example, so probably that’s what I messed up.

Why would I do that differently from the example? In general, but especially when working in a new programming language, I tend to prefer to call APIs by their fully qualified name, so instead of const { read } = require('fs') in Node.js or using std::cout; in C++, I prefer to type out fs.read() and std::cout << every time, so that I don’t get confused by all these bare identifiers in my program and have to wonder where they came from. I was trying to do that in this program as well, but apparently importing std::io::BufRead, even though I don’t use the BufRead identifier, is still necessary to provide the lines() method. Interesting, that means that my mental model of imports in Rust is wrong, and I’m not sure what is wrong about it. That will have to remain a mystery for the time being.

I could add another line saying use std::io::BufRead; as the error message suggests, but I see use std::io::{self, BufRead} in the example that I adapted, and since self has special syntax highlighting, I’m guessing that’s a shorthand for use std::io; use std::io::BufRead;. And indeed, if I change that, then the program works! I once again see all the numbers from the file.

Now for a bit of refactoring, because I’m not fond of the error handling pyramid in this code (like, println! indented at 4 indentation levels.) The previous example that I copied had this nice expect() call which I guess probably did some error handling under the hood, and there’s this ? operator in read_lines() which is probably abstracting away some error handling as well. I’ve dimly heard about the ? operator and the Result type before, so I’m not starting entirely from zero; I have a vague expectation that error handling in Rust should be concise. I will try changing the ifs in main() to the ? operator:

let lines = read_lines("input")?;
for line in lines {
    let ip = line?;
    println!("{}", ip);
}

This doesn’t work, but the error message is helpful about why not: “the ? operator can only be used in a function that returns Result or Option (or another type that implements Try)”.

What I actually want is to just abort the program if there’s an error, because for a short program like this I don’t actually care about recovering from errors. Maybe expect() will do that? Here’s what I will try next:

let lines = read_lines("input").expect("Bad file");
for line in lines {
    let ip = line.expect("Bad line in file");
    println!("{}", ip);
}

Great, this works. And to double-check what it does in the case of an error, I temporarily change input to inputzzzzz and indeed, the program “panics” (I guess “panic” means what I would call “assert” or “abort”?), and the panic message includes the string “Bad file”.

Confident that I know roughly how to deal with runtime errors, the next thing I want to change is that instead of printing each line as a string, I want to convert each line to an integer, and store it in an array of integers, and then print the array.

From other programming languages, I know roughly what I’ll have to do:

  • Figure out how to create an array when I don’t know the length in advance
  • Figure out how to convert a string to an integer
  • Figure out how to store an element in an array

I think I’ll start with the second item on that list, that seems easiest.

Maybe this time instead of googling, since the Rust documentation has already been so helpful, I’ll try to look this up in the “Rust by Example” page that I still have open in my browser. I go to Chapter 6, “Conversion”, and section 6.3, “To and from strings”. That looks promising.

Hmm, actually this is a bit confusing, it’s talking about “implementing traits” and “turbofish” (???) Aside from all that, from reading this page it looks like strings have a parse() method that converts them into integers, but I think I’ll google it after all, just to get a second opinion. Here’s where I land — this corroborates that I should use parse(), and as a bonus, gives a better explanation of what unwrap() does. So here’s my loop that prints integers:

for line in lines {
    let ip = line.expect("Bad line in file").parse::<i32>().unwrap();
    println!("{}", ip * 10);
}

(I added * 10 to make sure that I am actually printing integers and not strings. If I can multiply it by 10, then it must be a number.)

OK, now to create an array. First I google “rust array”, and click on another chapter of Rust by Example. From reading this, it seems that what I want is called a “slice”, not an array, because arrays have a length known in advance, and slices don’t.

But, unfortunately this example doesn’t tell how to create a slice except if you already have an array. So after two misses, I make a mental note to maybe avoid Rust by Example and stick to other parts of the Rust documentation.

Maybe what I want is more like what would be called a “vector” in C++? I google “rust vector” and land on another Rust by Example page after all! But this does look relevant, in fact it looks like exactly what I wanted. Vectors are called Vec in Rust.

Cribbing from the Vec example, my first attempt looks like this:

let entries: Vec<i32>;
for line in lines {
    entries.push(line.expect("Bad line in file").parse::<i32>().unwrap());
}
println!("{:?}", entries);

(I don’t know what the {:?} does differently than {}, but the example code uses it to print out vectors, so I do it too.)

This code doesn’t compile. The error message is “use of possibly-uninitialized entries“. At first I thought it was because lines might be empty and so you wouldn’t push any integer into entries before printing it out, but the error actually points to the push line. Apparently, Rust’s vector is not like C++ where std::vector<int> entries; has a default constructor that gives you an empty vector. I guess I need to know how to get an empty vector.

I click on the documentation link at the bottom, where it says “More Vec methods can be found under the std::vec module”. Now I’m really in the formal API documentation. This is not where I prefer to be when learning, I’d rather look at example code, but maybe this will help me. Sure enough, it does, and it looks like you can get an empty vector with vec![].

(As an aside, I’m wondering why there are these random exclamation marks scattered throughout Rust code, like in vec! and println!. It reminds me of Scheme code, where I also don’t understand the random exclamation marks. I wonder if they’re the same thing. The documentation page refers to vec! as a macro, so I’m guessing that might be it.)

I make that change and recompile, and the next error is that I didn’t declare entries as mutable. I go back to the Rust by Example page and see that if I’d read the example code more carefully I’d have noticed that I had to do that. So the line should read let mut entries: Vec<i32> = vec![]; and when I make that change, it works! I see the list of numbers printed out between square brackets and separated by commas, so it must be printing the vector.

I wonder if this can be done better? The example code says something about “collecting” an iterator into a vector with the collect() method, and I do know from reading the previous example that lines is an iterator! I wonder if it’s possible to take lines, map it through a string-to-int conversion function, and collect the result into my vector. The equivalent of what I’m thinking in Python would be entries = list(map(int, lines)).

First I’ll have to check if there’s a map() method similar to JavaScript’s Array.map() method or Python’s map() function. I google “rust iterator map”, and land here. I scroll down a bit, but this looks promising. The map() method looks like it does what I would expect from other languages, and as a bonus, now I know what the syntax for an inline function looks like in Rust.

Here’s my next attempt, and it looks much nicer:

let entries: Vec<i32> = read_lines("input")
    .expect("Bad file")
    .map(|s| s.expect("Bad line in file").parse::<i32>().unwrap())
    .collect();

I made one mistake before it would compile; I left out the type of entries. I thought maybe it could be inferred to be Vec<i32> since I am “collecting” an iterator of i32s, but I guess that’s not the case. Anyhow, this works, and it seems to be what I wanted!

Now that I’ve accomplished all the three things on my list, we have the mechanics out of the way, and I have to start thinking about how to solve the actual problem. It’s taken longer than I thought it might to get to this point, but that’s to be expected when learning a new language, and I think I should actually be surprised that it’s going this quickly!

So the puzzle is to find the two numbers in the list that add up to 2020, and multiply them. For this, I will need to check every pair of numbers in the list until I find the right pair, but the order doesn’t matter, so I can use a triangle pattern. (I know there’s an actual name for this, not “triangle pattern”, but I can’t think of it right now.) For example if the list had 4 numbers:

check 1 with 2, 3, 4
check 2 with    3, 4
check 3 with       4

In other words, the pairs are 1-2, 1-3, 1-4, 2-3, 2-4, and 3-4. Put more generally, I will need to loop from 0 to length − 2, inclusive2, and check the item at that index paired with each item that comes after it in the list.

Having read about slices earlier, I wonder if I can get a slice from a vector? Ideally it might look something like this in pseudocode:

for index, first in entries[start..length - 2, inclusive]:
    for second in entries[index + 1..end]:
        if first + second == 2020:
            print first * second
            exit with success
fail

Back to Google it is! I google “rust slice from vector” and land here. This looks promising! It seems that &vec[start..end] gives you a slice of vec, and the start and end indices are optional, defaulting to the start and end of the vector. I’m not sure if end is inclusive or exclusive, so I google “rust dot dot operator” and I find that .. is the “exclusive range operator”, whereas ..= is the “inclusive range operator”. I think it would be more expressive to write it as an inclusive range, so I start writing my first attempt. I also remember from reading the Vec page that you can get a pair of index and element by calling the enumerate() method of an iterator, so I use that in my outer loop because I need the index as well. Here’s the loop:

for (ix, first) in &entries[..=entries.len() - 2].enumerate() {
    for second in &entries[ix + 1..] {
        if (first + second == 2020) {
            println!("{} × {} = {}", first, second, first * second);
        }
    }
}

(I had seen earlier on the vec page that you get the length of a vector with len().) I’ll look up later how to exit the program once I find the result, but for now it’s OK to continue iterating even after I find the answer.

However, this doesn’t compile. For one thing I’m warned that I don’t need parentheses around the condition of the if statement — thanks, compiler! But the actual error is that enumerate() isn’t working, and this gives me the first error message I’ve seen so far that isn’t very helpful:

error[E0599]: no method named `enumerate` found for slice `[i32]` in the current scope
  --> src/main.rs:10:55
   |
10 |     for (ix, first) in &entries[..=entries.len() - 2].enumerate() {
   |                                                       ^^^^^^^^^ method not found in `[i32]`
   |
   = note: the method `enumerate` exists but the following trait bounds were not satisfied:
           `[i32]: Iterator`
           which is required by `&mut [i32]: Iterator`

I try using rustc --explain E0599 to find out what’s going on here, but it only tells me how to implement a nonexistent method on a type that I’ve defined. But I didn’t define the type [i32] myself, it’s a slice of integers, so I’m not sure how to add a method to it, and anyway that’s probably not the road I want to travel down! I have a feeling that this is a task where it’s overwhelmingly likely that there’s a built-in way to do it, and I just haven’t found it yet, so I really don’t want to go down a rabbit hole of extending built-in types.

So, time to go back to the documentation. I google “rust slice” and this time click through to the API docs3. I wonder if what I need is the iter() method, since the word “enumerate” is not present in this page according to Ctrl+F, and the error message is saying that enumerate() is an iterator method. Indeed, I change it to .iter().enumerate() and it still doesn’t work, but I get a different error which makes me think I’ve made some progress.

Here’s what the new error says:

error[E0277]: `&Enumerate<std::slice::Iter<'_, i32>>` is not an iterator
  --> src/main.rs:10:24
   |
10 |     for (ix, first) in &entries[..=entries.len() - 2].iter().enumerate() {
   |                        -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |                        |
   |                        `&Enumerate<std::slice::Iter<'_, i32>>` is not an iterator
   |                        help: consider removing the leading `&`-reference
   |
   = help: the trait `Iterator` is not implemented for `&Enumerate<std::slice::Iter<'_, i32>>`
   = note: `Iterator` is implemented for `&mut std::iter::Enumerate<std::slice::Iter<'_, i32>>`, but not for `&std::iter::Enumerate<std::slice::Iter<'_, i32>>`
   = note: required by `into_iter`

I’m not sure I understand this error, in fact I’m not actually sure what the & operator is for in Rust, but I’ll try doing what it says: remove the &. That makes the program work, hooray! I get an answer, and I enter it into the Advent of Code website, and it’s correct! So I’ve solved the puzzle.

I’d still like to try to make the program a bit better. We still need to exit when the answer is found, and also the ..=entries.len() - 2 looks kind of unreadable.

Tackling the latter point first, I wonder if Rust can do negative slice indices like Python does (for example, in Python arr[:-5] slices from the start to 5 before the end)? To find this out, I try googling a few things but fail to learn anything. Eventually I land on this Stack Overflow post.

(Hmm, the first Stack Overflow result! Normally when I google things, I get a lot more Stack Overflow. This could be because the Rust documentation is really comprehensive. Later as I edit these notes, it occurs to me that it could be because I’m googling very basic things that are generally covered in a language’s documentation, or maybe because Rust isn’t as popular as, say, JavaScript, where the basic questions are all answered multiple times on Stack Overflow.)

Anyway, it looks like negative slice indices are not possible. But from that Stack Overflow post, I click through to the documentation for split_last().

This looks complicated, so I think I’ll google “rust exit” first and come back to this later. I land in the documentation for std::process::exit. It seems I can use std::process; and process::exit(0), and for good measure I print out “Not found” and exit with code 1 if no solution is found.

OK, now back to split_last(). Since split_last() returns a tuple consisting of the last element and a slice of all the other elements, my first attempt looks like this: entries.split_last()[1].iter().enumerate(). This gives me the error “cannot index into a value of type Option<(&i32, &[i32])>” which I guess means that I have to handle the case where there aren’t enough elements. So, next I try entries.split_last().expect("Empty")[1].iter().enumerate() and that gives me this error:

error[E0608]: cannot index into a value of type `(&i32, &[i32])`
  --> src/main.rs:11:24
   |
11 |     for (ix, first) in entries.split_last().expect("Empty")[1].iter().enumerate() {
   |                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: to access tuple elements, use: `entries.split_last().expect("Empty").1`

Once again the compiler’s help is very helpful here. I replace [1] with .1 and it works.

I’m satisfied with this now, it could probably be made a lot better, but it finds the answer and it seems reasonably tidy (here, I’d define “tidy” as “doesn’t reimplement things that we can use the language facilities and standard library for”.)

This is the full code:

use std::fs;
use std::io::{self, BufRead};
use std::path;
use std::process;

fn main() {
    let entries: Vec<i32> = read_lines("input")
        .expect("Bad file")
        .map(|s| s.expect("Bad line in file").parse::<i32>().unwrap())
        .collect();
    for (ix, first) in entries.split_last().expect("Empty").1.iter().enumerate() {
        for second in &entries[ix + 1..] {
            if first + second == 2020 {
                println!("{} × {} = {}", first, second, first * second);
                process::exit(0);
            }
        }
    }
    println!("Not found");
    process::exit(1);
}

fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<fs::File>>>
where
    P: AsRef<path::Path>,
{
    let file = fs::File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}

Day 1, Part 2

The second part of the Day 1 puzzle is a slightly more complicated variation on the first part: now we have to find the three numbers in the list that add up to 2020, and multiply all three of them together for the answer.

There’s probably a smart way to do this, but … (train of thought derails)

I was just about to write “I’m going to do it the most straightforward way and just do another triangle iteration”, but then I thought, hmm, maybe this isn’t as “tidy” as I thought it was after all. If I were doing this in Python, it seems like Python’s itertools module would have a way to iterate over all possible pairs in a list, and I wonder if Rust has something similar that would be generalizable to triplets? Suddenly I remember the word I was trying to think of in Part 1 when I said “triangle pattern”: permutations. I google “rust iterate over permutations” and find out that Rust also has an itertools module in the standard library. (Actually permutations is wrong, we actually want combinations and you’d think that as a former physicist I’d know that!4 Luckily, itertools has a method for both of them, so I land in the right place anyway.)

So, it seems that instead of working on the second part of the puzzle, surprise! I will actually go back and improve the first part! To start out, I go read the documentation for Itertools.combinations(). My first attempt is to add use itertools::Itertools; to the top of the file, and make the loop look like this:

for (first, second) in entries.iter().combinations(2) {
    if first + second == 2020 {
        println!("{} × {} = {}", first, second, first * second);
        process::exit(0);
    }
}

However, that doesn’t compile: “use of undeclared crate or module itertools“. I guess I read it wrong, Itertools is not actually part of the standard library, it’s a package.

So how do I include a package in my program? I’m pretty sure Cargo has something to do with it. I go back to the “getting started with Rust” page that I visited several hours ago, because I remember it said something about this. And indeed, it tells me to add the package to the Cargo.toml file that cargo new automatically generated.

I open Cargo.toml and add itertools under [dependencies] — the example adds a version number as well, but I hope that just putting the name will get me the latest version. I find out that it does not, and what’s more, it’s even invalid syntax. So I add itertools = "0.9.0" instead, which I determined from the Itertools documentation is the latest version as of this writing.

This build takes longer — presumably because it’s downloading itertools — and now I get this error:

error[E0308]: mismatched types
  --> src/main.rs:12:9
   |
12 |     for (first, second) in entries.iter().combinations(2) {
   |         ^^^^^^^^^^^^^^^    ------------------------------ this expression has type `Vec<&i32>`
   |         |
   |         expected struct `Vec`, found tuple
   |
   = note: expected struct `Vec<&i32>`
               found tuple `(_, _)`

Hmmm, I understand about half of what this means. Reading the documentation for combinations() a bit more carefully, it looks like the n-tuples it returns are vectors, and apparently you can’t use destructuring assignment on vectors like that in Rust.

I start to google “rust destructure vector” but then I notice in the Itertools documentation that there’s a tuple_combinations() method that does the same thing as combinations(), only with tuples. That sounds like what I need, so I try that replacing combinations() with tuple_combinations().

That doesn’t work either, and the error message tells me that tuple_combinations() takes no arguments. Huh, no arguments? How does it know how long to make the tuples then? But once again I go back and read the documentation more carefully, and it looks like the tuple length is a template parameter (or whatever that is called in Rust) instead of an argument. I guess that makes sense, because the compiler has to know the length at compile time. I try tuple_combinations<2>() and apparently that’s a C++-ism because it doesn’t work either, but the compiler helpfully tells me to insert a double-colon: tuple_combinations::<2>().

However, that’s not right either: it wants a type parameter, not a number. For the third time I go back and read the documentation more carefully, which I really should have done in the first place, haha! What I now think I understand is that it will infer the length of the tuple from the number of variables that I destructure it into. So, my first assumption about <2> was totally wrong, and I probably got it from C++. In the end, this is what works: for (first, second) in entries.iter().tuple_combinations() { and with that I get back my original correct answer.

Now I notice that we are calling collect() on an iterator to make it into a vector, only to call iter() on it and make it back into an iterator. It seems like I should be able to avoid that. I try this because it seems like it ought to work:

let pairs = read_lines("input")
    .expect("Bad file")
    .map(|s| s.expect("Bad line in file").parse::<i32>().unwrap())
    .tuple_combinations();
for (first, second) in pairs {

But this gives me a confusing error about Clone not being implemented, and I decide not to pursue this any longer, because I want to get on to the second part of the puzzle which I think I can solve easily now. So I put collect() and iter() back in.

Here’s the full code of my new, improved solution to the first part of the puzzle:

use itertools::Itertools;
use std::fs;
use std::io::{self, BufRead};
use std::path;
use std::process;

fn main() {
    let entries: Vec<i32> = read_lines("input")
        .expect("Bad file")
        .map(|s| s.expect("Bad line in file").parse::<i32>().unwrap())
        .collect();
    for (first, second) in entries.iter().tuple_combinations() {
        if first + second == 2020 {
            println!("{} × {} = {}", first, second, first * second);
            process::exit(0);
        }
    }
    println!("Not found");
    process::exit(1);
}

fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<fs::File>>>
where
    P: AsRef<path::Path>,
{
    let file = fs::File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}

Now to solve the second puzzle, with triplets instead of pairs, I should just be able to change the loop to this:

for (first, second, third) in entries.iter().tuple_combinations() {
    if first + second + third == 2020 {
        println!(
            "{} × {} × {} = {}",
            first,
            second,
            third,
            first * second * third
        );
        process::exit(0);
    }
}

Let’s see if it works! (It does, hooray!)

Just for posterity, I change the code in puzzle1-1 back to pairs, and create a new project (cargo new puzzle1-2), copy over the files, and then put the triplet code into puzzle1-2. (Later, I published it to GitHub if you want to browse it.)

I’m happy with this solution! There are probably ways it could be improved, like the collect()/iter() dead-end that I ran into, and those expect()s littered throughout the code don’t look idiomatic, but it got me the correct answers to the puzzles and it doesn’t look too clunky.

Having said that about not being too clunky; just for comparison, here’s what I’d write in a language that I’m more familiar with, like Python (although this is with the benefit of hindsight, after having done the exercise in Rust; I always forget to reach for itertools first in Python when I have an iterator-like problem. The code below is definitely not something that I’d just bust out in a few minutes normally.)

import itertools
import sys

with open('input', 'r') as lines:
    pairs = itertools.combinations(map(int, lines), 2)
    for first, second in pairs:
        if first + second == 2020:
            print(f'{first} × {second} = {first * second}')
            sys.exit(0)
print('Not found')
sys.exit(1)

You can see that the Python code is still quite a lot more streamlined than the Rust code, so I suspect I’m not yet writing very good Rust code. I hope that improves as I continue the exercises!

Afterword

From writing these notes I even learned something new about my thought process: that when I google up some documentation, I don’t read it as carefully as I thought I did!

Another thing that I noticed is that while it was important to know what permutations and combinations are, I didn’t actually have to know how to compute them, let alone an algorithm to compute them efficiently. I tried a simple algorithm which worked, but I could have saved myself a lot of time and trouble if I hadn’t even tried to write the algorithm myself, and instead just looked for Itertools! (This is why it’s always a mystery to me why some technical interviewers expect you to have algorithm knowledge at your fingertips. Maybe I’m biased because I don’t have a computer science degree, but I don’t think it’s how programmers realistically do their jobs!)

It’s also interesting to note that this took me the whole afternoon, but I think five years ago, in 2015, it would have taken a lot longer. When I thought about what I was thinking about all afternoon, I realized a surprising thing! A big part of the reason I could do this exercise in one afternoon is not that I have become better at programming, but that I have become better at knowing what to google.

Ten years ago, in 2010, I wouldn’t even have dared to try this exercise with a new programming language! I remember it clearly because 2010 was when I learned Python for the first time. I did it by going through the tutorial in the Python documentation. Even that was a novel experience that left me unsure of myself, because at the time I still believed that the only way for me to learn a programming language was to get a reference book, and read it cover to cover.5 But who has time for that anymore, anyway? Certainly not me!

I wouldn’t say by any means that doing a programming exercise like this means I’ve “learned” Rust, but what I hope to achieve by doing Advent of Code is that by the end, I’ll have gained enough practical experience in Rust that I might feel confident enough to reach for it the next time I need to solve a real-life programming problem for which I might otherwise use C or C++. And that will be the next step to really learning it.


[1] Interestingly, I think I actually googled less than I normally would, because the error messages from the Rust compiler are so helpful ↩️

[2] Minus 2 because the last item in the list is at index length − 1, and by the time I get there I won’t have any numbers left to pair it with ↩️

[3] I didn’t realize until editing my notes into a blog post, that there are actually two different documentation pages about slices, std::slice and “primitive slice type” ↩️

[4] You might think that, but you’d be wrong ↩️

[5] I learned C that way, and it stuck; I also learned Perl that way, and it didn’t. After doing the Python tutorial I did eventually get a Python book and inhaled it, but the seed of a new way of life was already planted ↩️

Advertisement

6 thoughts on “Advent of Rust

  1. This was so enjoyable for me to read on many levels. Last year I tried my hand at learning rust while trying to get a feature into flat-manager and stumbled along in a very similar path, but I suspect you moved a lot faster than I did. More importantly, bravo to you for putting this out there. This is exactly how things go for (I suspect) 95% of developers, but I know I would have a lot of trouble airing my dirty laundry, so to speak. Also, I commend you for your copious note taking. I always just plow along without the patience to take notes but wish I had them later.

    Finally, this also resonated with me because like you I don’t have formal computer science education and I always wonder how that affects my uptake. In particular, I’ve wondered about the algorithms part many times. It’s true that you would almost never write some algorithm from scratch in a real world program. However, if I consider my electrical engineering background, I always felt like strong understanding of the fundamentals gave me better understanding and more confidence even if I rarely or never had to directly use those skills.

    Anyways, thanks for doing this!

    • I really appreciated this comment. I hadn’t thought of it as airing dirty laundry, but I guess that is kind of what it is! I would hope that having more dirty laundry out there would help to adjust expectations that this really is how a lot of people learn. So to speak, normalizing what’s already quietly normal.

      I don’t feel like I’m taking much of a risk by writing this; I suspect I’ve got enough of a track record that no-one is going to judge me for it. I think you do too, to be honest. For sure, I would not do this if I were in a more beginning stage as a software developer.

  2. Have you found “cargo clippy” yet? It takes rust’s normally pretty good error messages and gives you random advice about how to improve your program, for example instead of saying “foo.unwrap_or(Vec::new())” it’ll recommend the more efficient “foo_unwrap_or_else(Vec::new)”. I found it a real help when learning as it taught me all kinds of tricks and made my code feel more idiomatic.

    The other thing I constantly find useful when learning is this diagram: https://docs.google.com/drawings/d/1EOPs0YTONo_FygWbuJGPfikO9Myt5HwtiFUHRuE1JVM/preview by David Drysdale (Note the bits are clickable to get the docs!)

  3. Pingback: December of Rust 2021, Part 1: A Little Computer | The Mad Scientist Review

  4. Pingback: December of Rust Project, Part 2: The Assembler Macro | The Mad Scientist Review

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.