Day 1 (Advent of Code 2022)

👋 This page was last updated ~2 years ago. Just so you know.

Two years ago, I did part of Advent of Code 2020 using the Rust language. It was a lot of fun, so let's try it again!

The problem statement

Our input looks something like this:

1000
2000
3000

4000

5000
6000

7000
8000
9000

10000

Each group of lines separated by an empty line is a list of food items an elf is carrying: each line corresponds to the number of calories in that food.

So, the first elf is carrying three food items: one is 1000 calories, the second is 2000 calories, the third is 3000 calories. The second elf is carrying only one food item of 4000 calories, etc.

We must answer "how many calories is the elf who carries the most calories carrying?"

This'll be a short one!

Getting started

If you're planning on following along, you'll need to install a Rust toolchain. The simplest way is through rustup.

If everything is set up correctly, you should be able to run these commands in a terminal:

$ rustc -V
rustc 1.65.0 (897e37553 2022-11-02)

$ cargo -V
cargo 1.65.0 (4bc8f24d3 2022-10-20)
Cool bear

Cool bear's hot tip

If this is showing something older than 1.65, it might be a while since you last did some Rust. Make sure to run rustup update first!

Me, I've made an aoc2022 folder, initialized a Git repository in there and pushed it to a private GitHub repo (no spoilers!). In there, I've made a new day1 Rust crate:

$ cd aoc2022/

$ cargo new day1
     Created binary (application) `day1` package

I've also created a file in aoc2022/.gitignore with this line:

target

So that any folders or files named target won't be pushed to GitHub.

Cool bear

Cool bear's hot tip

cargo new would normally generate a .gitignore itself (and an empty Git repository) but it doesn't here because the outer directory is already a repository, so it assumes you're just creating a sub-crate or something.

If you don't use Git, you can configure cargo to use another VCS, see the cargo-new.vcs configuration option.

Our final file structure looks like this:

$ tree $PWD
/home/amos/bearcove/aoc2022
├── day1
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── README.md

2 directories, 3 files

The day1 project generated is just a hello world, let's run it:

$ cd day1/
$ cargo run
   Compiling day1 v0.1.0 (/home/amos/bearcove/aoc2022/day1)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/day1`
Hello, world!

We'll put the problem statement's sample input in there, and later on I'll replace it with my personal input file (that's how AOC works: we all get our own inputs, so to cheat you need at the very least to use someone else's code, you can't just paste their answer).

So, in day1/src/input.txt, let's paste this:

1000
2000
3000

4000

5000
6000

7000
8000
9000

10000
Cool bear

Cool bear's hot tip

As far as code editors go, if you already have a strong preference, so be it. If you don't, Visual Studio Code is the low-friction option, and it runs great on Windows, macOS and Linux.

Whatever you use, make sure you have rust-analyzer installed and configured correctly.

In particular, you may want to enable as many inlay hints as you're comfortable with: they let you see types, lifetimes, argument names at callsites, etc. They make your code view a little busier, but they're also a great help.

There's a couple ways we can read that file: we can read it at runtime.

fn main() {
    let input = std::fs::read_to_string("src/input.txt").unwrap();
    println!("{input}");
}

Types and error handling

Why .unwrap()?

std::fs::read_to_string returns an std::io::Result<String>, which is an alias for std::result::Result<String, std::io::Error>.

Result<T, E> is an enum, also called "sum type": it can be either the Ok(T) variant (where T is the type of the value we get if everything went well), or the Err(E) variant (where E is the type of the value we get if something went wrong).

Here, if std::fs::read_to_string goes well, we get a String. In fact, if you have inlay hints enabled, where you have this code:

    let input = std::fs::read_to_string("src/input.txt").unwrap();

You actually see this:

    let input: String = std::fs::read_to_string("src/input.txt").unwrap();
    //       ~~~~~~~~

You can play around with that line of code: if you remove the .unwrap(), your file contains this:

    let input = std::fs::read_to_string("src/input.txt");

But your code editor shows this:

    let input: Result<String, Error> = std::fs::read_to_string("src/input.txt");
    //       ~~~~~~~~~~~~~~~~~~~~~~~

And that's one of the big things about Rust: our love language is types, and I think that's beautiful. read_to_string doesn't "throw an exception", it returns a value that lets us know if things went fine or not.

And most of the time, in Rust, we're trying to figure out how to go from one type to another type — it can get frustrating, but it's also a wonderful set of guardrails.

We can't accidentally get a String and "forget" to check the error (a problem you could have in a language where errors are passed in another channel, cf. Go's multi-valued returns, or C's "return a non-zero int to signal failure" pattern).

If we want to ignore the error, we have to be really explicit about it:

    let input = match std::fs::read_to_string("src/input.txt") {
        Ok(s) => s,
        Err(e) => {
            // let's ignore the error and return the empty string
            "".into()
        }
    };

Again here, inlay type hints give us more information about what's really happening here:

    let input: String = match std::fs::read_to_string("src/input.txt") {
//           ~~~~~~~~
        Ok(s: String) => s,
//          ~~~~~~~~
        Err(e: Error) => {
//           ~~~~~~~
            // let's ignore the error and return the empty string
            "".into()
        }
    };

And in fact, the compiler specifically warns us that we're not doing anything with the e: Error we've "destructured" in an "arm" of the "match" expression:

$ cargo check
    Checking day1 v0.1.0 (/home/amos/bearcove/aoc2022/day1)
warning: unused variable: `e`
 --> src/main.rs:4:13
  |
4 |         Err(e) => {
  |             ^ help: if this is intentional, prefix it with an underscore: `_e`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: `day1` (bin "day1") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.19s

...and lets us know 1) which warning it is (unused_variables, which we can turn off altogether — probably a bad idea — with the #[allow(unused_variables)] attribute) and 2) how to get rid of it, by renaming e to _e for example, or even just _ to throw it away completely.

But if you look at that code, we're already forced to handle both codepaths anyway:

    let input = match std::fs::read_to_string("src/input.txt") {
        Ok(s) => s,
        Err(e) => {
            // let's ignore the error and return the empty string
            "".into()
        }
    };

So we might as well handle the error properly anyway. For example, we could print a message (and maybe a backtrace), and safely stop the program's execution:

    let input = match std::fs::read_to_string("src/input.txt") {
        Ok(s) => s,
        Err(e) => panic!("Couldn't read src/input.txt: {e}"),
    };

Which would look like this:

$ cargo run
   Compiling day1 v0.1.0 (/home/amos/bearcove/aoc2022/day1)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/day1`
thread 'main' panicked at 'Couldn't read src/input.txt: No such file or directory (os error 2)', src/main.rs:4:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Or like this, if we set the RUST_BACKTRACE environment variable to 1. (Shown here for zsh/bash, might look different on, say, PowerShell):

$ RUST_BACKTRACE=1 cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/day1`
thread 'main' panicked at 'Couldn't read src/input.txt: No such file or directory (os error 2)', src/main.rs:4:19
stack backtrace:
   0: rust_begin_unwind
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
   1: core::panicking::panic_fmt
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
   2: day1::main
             at ./src/main.rs:4:19
   3: core::ops::function::FnOnce::call_once
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

And that's what Result::unwrap does for us.

Instead of writing this:

    let input = match std::fs::read_to_string("src/input.txt") {
        Ok(s) => s,
        Err(e) => panic!("{e}"),
    };

We can write this:

    let input = std::fs::read_to_string("src/input.txt").unwrap();

And the result is about the same:

$ RUST_BACKTRACE=1 cargo run
   Compiling day1 v0.1.0 (/home/amos/bearcove/aoc2022/day1)
    Finished dev [unoptimized + debuginfo] target(s) in 0.33s
     Running `target/debug/day1`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:2:58
stack backtrace:
   0: rust_begin_unwind
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
   1: core::panicking::panic_fmt
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
   2: core::result::unwrap_failed
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/result.rs:1785:5
   3: core::result::Result<T,E>::unwrap
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/result.rs:1107:23
   4: day1::main
             at ./src/main.rs:2:17
   5: core::ops::function::FnOnce::call_once
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Note that our error is lacking context here. We could use Result::expect instead of Result::unwrap:

fn main() {
    let input = std::fs::read_to_string("src/input.txt").expect("while reading src/input.txt");
    println!("{input}");
}
$ cargo run
   Compiling day1 v0.1.0 (/home/amos/bearcove/aoc2022/day1)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/day1`
thread 'main' panicked at 'while reading src/input.txt: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:2:58
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

...but that's not great either.

Because maybe we've put all the file reading in its own function:

fn main() {
    let input = read_input();
    println!("{input}");
}

fn read_input() -> String {
    std::fs::read_to_string("src/input.txt").expect("while reading src/input.txt")
}

...and we don't want our read_input function to panic (via Result::expect) if read_to_string returns a Result::Err(E).

Instead, we'd like the read_input to propagate the error, like so:

fn read_input() -> Result<String, std::io::Error> {
    std::fs::read_to_string("src/input.txt")
}

But then we have to handle the error at the callsite, in main, like so:

fn main() {
    let input = read_input().unwrap();
    println!("{input}");
}

And we once again lose context:

$ cargo run
   Compiling day1 v0.1.0 (/home/amos/bearcove/aoc2022/day1)
    Finished dev [unoptimized + debuginfo] target(s) in 0.33s
     Running `target/debug/day1`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:2:30
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

It doesn't say which file or directory wasn't found. And doing .expect("while reading src/input.txt") in the main function feels wrong: it's an implementation detail of the read_input function, main shouldn't have to know about it.

So, what can we do?

We can add some context to our error! We can do that by making our own error type:

/// An [std::io::Error] associated with a path
struct PathedIoError {
    path: String,
    inner: std::io::Error,
}

fn read_input() -> Result<String, PathedIoError> {
    let path = "src/input.txt";
    match std::fs::read_to_string(path) {
        Ok(s) => Ok(s),
        Err(e) => Err(PathedIoError {
            path: path.into(),
            inner: e,
        }),
    }
}

If we do this, we run into further compiler errors:

$ cargo check
    Checking day1 v0.1.0 (/home/amos/bearcove/aoc2022/day1)
error[E0277]: `PathedIoError` doesn't implement `Debug`
    --> src/main.rs:2:17
     |
2    |     let input = read_input().unwrap();
     |                 ^^^^^^^^^^^^ ------ required by a bound introduced by this call
     |                 |
     |                 `PathedIoError` cannot be formatted using `{:?}`
     |
     = help: the trait `Debug` is not implemented for `PathedIoError`
     = note: add `#[derive(Debug)]` to `PathedIoError` or manually `impl Debug for PathedIoError`
note: required by a bound in `Result::<T, E>::unwrap`
    --> /home/amos/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:1103:12
     |
1103 |         E: fmt::Debug,
     |            ^^^^^^^^^^ required by this bound in `Result::<T, E>::unwrap`
help: consider annotating `PathedIoError` with `#[derive(Debug)]`
     |
7    | #[derive(Debug)]
     |

For more information about this error, try `rustc --explain E0277`.
error: could not compile `day1` due to previous error

Returning an Err(E) where E was std::io::Error worked, because std::io::Error implemented all the right "traits". A trait is often just a list of functions a type can implement, here's the Debug trait it's complaining about for example:

// somewhere in the rust standard library

pub trait Debug {
    /// Formats the value using the given formatter.
    ///
    /// (cut)
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}

(Result here is naked because in the file where the Debug trait is declared, it's aliased to std::fmt::Result, which is an alias for std::result::Result<(), std::fmt::Error>. The "happy path" type is (), the empty tuple, which is just nothing, and the "unhappy path" type is a formatting error).

So, let's summarize what we know:

  • types can implement traits
  • traits are collections of methods
  • the std::io::Error type implements the std::fmt::Debug trait
  • our PathedIoError type does not
  • we must fix that

Why must we fix that? Because we're calling Result::unwrap here:

fn main() {
//                             👇
    let input = read_input().unwrap();
    println!("{input}");
}

And Result::unwrap has this type signature:

// somewhere in the rust standard library

impl<T, E> Result<T, E> {
    pub fn unwrap(self) -> T
    where
        E: fmt::Debug,
    {
      // (cut: implementation)
    }
}

See that where E: fmt::Debug? That's a "trait bound".

Result<T, E> is a generic type: it can exist for any type T and E. In our case, the concrete type read_input is returning is Result<String, PathedIoError>: it's right there in our code:

fn read_input() -> Result<String, PathedIoError> {
//                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}

So, in Result<T, E> we have T = String and E = PathedIoError, and, for a laugh, we can do the compiler's job by performing "monomorphization" of the impl block we saw above.

Replacing T and E with our concrete types, we have:

impl Result<String, PathedIoError> {
    pub fn unwrap(self) -> String
    where
        PathedIoError: fmt::Debug,
    {
      // (cut: implementation)
    }
}

And here, it's a little clearer where the error comes from. We don't yet know what the Debug trait is really for, or why Result::unwrap wants it for its E type parameter, but we know we have to do something like:

impl std::fmt::Debug for PathedIoError {
  // ???
}

And if we type that in, the compiler complains: "not all trait items implemented, missing: fmt".

But rust-analyzer offers a "quick fix" called "Implementing missing members":

A VS Code screenshot showing an empty impl Debug for PathedIoError, with a lightbulb, and the Quick Fix menu shown

Cool bear

Cool bear's hot tip

You can summon that "Quick Fix" menu by clicking on the light bulb icon, or pressing Ctrl+. (Cmd+. on macOS). If you're not using VS Code, you get to find out how to invoke it on your own! How fun.

The inline error message you see here is provided by VS Code's Error Lens extension.

And it generates this:

impl std::fmt::Debug for PathedIoError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("PathedIoError")
            .field("path", &self.path)
            .field("inner", &self.inner)
            .finish()
    }
}

And that's enough to make our program compile and run:

$ cargo run
   Compiling day1 v0.1.0 (/home/amos/bearcove/aoc2022/day1)
    Finished dev [unoptimized + debuginfo] target(s) in 0.33s
     Running `target/debug/day1`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: PathedIoError { path: "src/input.txt", inner: Os { code: 2, kind: NotFound, message: "No such file or directory" } }', src/main.rs:2:30
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

And here we have the context: we have the path ("src/input.txt") and we have the inner error, an std::io::Error with a code, a kind, and a message.

Here's the thing though: the Debug implementation that rust-analyzer generated is exactly the same as what would be generated by the Debug derive macro.

Cool bear

Oh no, more concepts

Which is to say, we can replace this:

/// An [std::io::Error] associated with a path
struct PathedIoError {
    path: String,
    inner: std::io::Error,
}

impl std::fmt::Debug for PathedIoError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("PathedIoError")
            .field("path", &self.path)
            .field("inner", &self.inner)
            .finish()
    }
}

With just this:

/// An [std::io::Error] associated with a path
#[derive(Debug)] // 👈
struct PathedIoError {
    path: String,
    inner: std::io::Error,
}

...but also, this isn't the friendliest way to format that error.

Maybe we'd better provide our own Debug implementation, that prints something more human-readable:

impl std::fmt::Debug for PathedIoError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "for file {:?}: {}", self.path, self.inner)
    }
}

// (don't forget to remove `#[derive(Debug)]` on top of `struct PathedIoError`)
$ cargo run
   Compiling day1 v0.1.0 (/home/amos/bearcove/aoc2022/day1)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/day1`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: for file "src/input.txt": No such file or directory (os error 2)', src/main.rs:2:30
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Cool bear

That is friendlier.

However, I don't really fancy having to write this for every one of my projects.

As it turns out, there's Rust crates (libraries) that do just that: fs-err is one of them, let's add a dependency on it:

$ cargo add fs-err
    Updating crates.io index
      Adding fs-err v2.9.0 to dependencies.
             Features:
             - io_safety
             - tokio

What this did was look up the latest version of fs-err on https://crates.io and add it to our Cargo.toml manifest:

[package]
name = "day1"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
fs-err = "2.9.0" # 👈

And then our whole program can just be this:

fn main() {
    let input = read_input().unwrap();
    println!("{input}");
}

fn read_input() -> Result<String, std::io::Error> {
    // now from the `fs-err` crate, rather than `std::fs`
    fs_err::read_to_string("src/input.txt")
}
$ cargo run
   Compiling fs-err v2.9.0
   Compiling day1 v0.1.0 (/home/amos/bearcove/aoc2022/day1)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/day1`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Custom { kind: NotFound, error: Error { kind: OpenFile, source: Os { code: 2, kind: NotFound, message: "No such file or directory" }, path: "src/input.txt" } }', src/main.rs:2:30
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

And we can see that fs-err is doing essentially the same thing we were doing, except it's wrapping its Error type in std::io::Error.

Here's another way to tackle this: let's remove fs-err from our Cargo.toml manifest with cargo rm fs-err (or by directly editing the file), and add a dependency on color-eyre instead:

$ cargo add color-eyre
    Updating crates.io index
      Adding color-eyre v0.6.2 to dependencies.
             Features:
             + capture-spantrace
             + color-spantrace
             + tracing-error
             + track-caller
             - issue-url
             - url

color-eyre provides us with its own color_eyre::Result<T> type, which is an alias for std::result::Result<T, color_eyre::Report>. Most error types can be converted to color_eyre::Report, so, for example, we can do this:

fn read_input() -> color_eyre::Result<String> {
    let input = std::fs::read_to_string("src/input.txt")?;
    Ok(input)
}

Or this:

fn read_input() -> color_eyre::Result<String> {
    std::fs::read_to_string("src/input.txt").map_err(|e| e.into())
    //                                               ~~~~~~~~~~~~
    //                                 this is a closure 👆
    //
    //                                (inlay type hints should show you
    //                                `e: std::io::Error`)
}

Or this:

fn read_input() -> color_eyre::Result<String> {
    std::fs::read_to_string("src/input.txt").map_err(From::from)
}

Because the conversion from std::io::Error to color_eyre::Report is possible thanks to this impl block:

impl<E> From<E> for Report
where
    E: StdError + Send + Sync + 'static,
{
    #[cfg_attr(track_caller, track_caller)]
    fn from(error: E) -> Self {
        Report::from_std(error)
    }
}

There's a lot more going on here that we're not going to cover, but, just like Debug is a common trait that means a type can be formatted in a somewhat-verbose way, From is a common trait that expresses that "infallible conversions" can occur between two types.

It is closely related to the Into trait, that converts in the other direction (and is what lets us do e.into())

Me personally, I prefer this form:

fn read_input() -> color_eyre::Result<String> {
    let input = std::fs::read_to_string("src/input.txt")?;
    Ok(input)
}

Because we often want to do other things with the value, so it's useful to bind it to a name with let (here, the binding is named input).

As-is, our program (when src/input.txt does not exist) prints this:

$ cargo run --quiet
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: No such file or directory (os error 2)

Location:
    src/main.rs:7:17', src/main.rs:2:30
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Cool bear

So we're back to square one? There's no context here.

That's true, although it's somewhat prettier. And it shows where the error was actually built (main.rs:7:17, which is main.rs line 7 column 17), not just where it was handled (main.rs:2:30).

If we install the color-eyre handler, it's prettier still:

fn main() {
    // here 👇
    color_eyre::install().unwrap();

    let input = read_input().unwrap();
    println!("{input}");
}

fn read_input() -> color_eyre::Result<String> {
    let input = std::fs::read_to_string("src/input.txt")?;
    Ok(input)
}
$ cargo run --quiet
The application panicked (crashed).
Message:  called `Result::unwrap()` on an `Err` value:
   0: No such file or directory (os error 2)

Location:
   src/main.rs:9

Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
Location: src/main.rs:4

Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.

Here it is with colors:

The same error as above, but with colors: red, cyan, and purple

Still, we don't have context. So let's add some!

// 👇 this `use` directive was added automatically by rust-analyzer when...
use color_eyre::eyre::Context;

fn main() {
    color_eyre::install().unwrap();

    let input = read_input().unwrap();
    println!("{input}");
}

fn read_input() -> color_eyre::Result<String> {
    // ...I did `Ctrl+Space` after typing `.wrap_e` to insert this call 👇
    let input = std::fs::read_to_string("src/input.txt").wrap_err("reading src/input.txt")?;
    Ok(input)
}
$ cargo run --quiet
The application panicked (crashed).
Message:  called `Result::unwrap()` on an `Err` value:
   0: reading src/input.txt
   1: No such file or directory (os error 2)

Location:
   src/main.rs:11

Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
Location: src/main.rs:6

Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.

There! To simplify some more, we can even have main itself return a Result, instead of calling .unwrap() from there.

Let's try it out:

use color_eyre::eyre::Context;

fn main() -> color_eyre::Result<()> {
    //       was unwrap 👇
    color_eyre::install()?;

    //                 was unwrap 👇
    let input = read_input()?;
    println!("{input}");

    // now we must return a Result
    Ok(())
}

fn read_input() -> color_eyre::Result<String> {
    let input = std::fs::read_to_string("src/input.txt").wrap_err("reading src/input.txt")?;
    Ok(input)
}
$ cargo run --quiet
Error:
   0: reading src/input.txt
   1: No such file or directory (os error 2)

Location:
   src/main.rs:16

Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.

And that got rid of the duplicate "Backtrace omitted" messages. The only source location that's shown by default is where the error was constructed, here it's in read_input.

And that's all we need to know to solve this advent of code day 1 exercise with Rust.

Cool bear

Wait, what? We haven't solved it at all, not even a little. One might say we got distracted.

No no, on the contrary, we've just gone through a bunch of essential concepts in Rust. Values are of a certain type, types can have methods and they can implement traits. Some types and methods are generic, and can operate on any type that satisfy "trait bounds".

Our job is to turn types into other types, and that'll help us solve the task at hand.

Cool bear

If.. if you say so.

Okay, let's solve the problem then!

Iterators and for loops

First off, let's turn "reading the file" from a run-time problem (when the program is executed) into a compile-time problem (when the executable is generated by cargo / rustc / the linker), using the include_str macro:

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let input = include_str!("input.txt");
    println!("{input}");

    Ok(())
}

This does essentially the same thing, except that now, input.txt doesn't need to be present when we run the program: it's baked into it.

Cool bear

So we did... all this... for nothing?

Oh no bear, not nothing. We did all this as a gentle... as a rather brutal introduction to types, and traits, and stuff.

See now for example, if we do this:

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let input = include_str!("input.txt");
    let lines = input.lines();

    Ok(())
}

We're not sure what to do with that lines, right? Well, our code editor tells us it's of type Lines:

    let lines: Lines = input.lines();
//           ~~~~~~~

Which helps because uhh... ok by itself that doesn't help much. But we can open the context menu on lines (with right click, most likely) and use the "Go To Type Definition" command and land in the middle of the standard library:

// in `rustlib/src/rust/library/core/src/str/iter.rs`

/// An iterator over the lines of a string, as string slices.
///
/// This struct is created with the [`lines`] method on [`str`].
/// See its documentation for more.
///
/// [`lines`]: str::lines
#[stable(feature = "rust1", since = "1.0.0")]
#[must_use = "iterators are lazy and do nothing unless consumed"]
#[derive(Clone, Debug)]
pub struct Lines<'a>(pub(super) Map<SplitTerminator<'a, char>, LinesAnyMap>);

#[stable(feature = "rust1", since = "1.0.0")]
impl<'a> Iterator for Lines<'a> {
    type Item = &'a str;

    #[inline]
    fn next(&mut self) -> Option<&'a str> {
        self.0.next()
    }

    // (cut)
}

Ah, it's an iterator: there's a next method here, we can probably do something with that:

    let mut lines = input.lines();
    loop {
        let maybe_line = lines.next();
        match maybe_line {
            Some(line) => {
                println!("Got line: {}", line);
            }
            None => break,
        }
    }

This is a lot more verbose than it needs to be, but might help draw a closer parallel to some other languages.

Again, inlay type hints give us some info about the next method:

        let maybe_line: Option<&str> = lines.next();
//                    ~~~~~~~~~~~~~~

Option<T> is an enum, similar to Result<T, E>, but simpler. There's Some(T) (something) and None (nothing). And we use a match expression with two arms to "destructure it", just like we did with results before.

match is just one of the ways we can do pattern matching though: if-let is another one:

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let input = include_str!("input.txt");

    let mut lines = input.lines();
    loop {
        if let Some(line) = lines.next() {
            println!("Got line: {}", line);
        } else {
            break;
        }
    }

    Ok(())
}

But wait.. what's this?

$ cargo clippy
warning: this loop could be written as a `while let` loop
  --> src/main.rs:7:5
   |
7  | /     loop {
8  | |         if let Some(line) = lines.next() {
9  | |             println!("Got line: {}", line);
10 | |         } else {
11 | |             break;
12 | |         }
13 | |     }
   | |_____^ help: try: `while let Some(line) = lines.next() { .. }`
   |
   = note: `#[warn(clippy::while_let_loop)]` on by default
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#while_let_loop

warning: `day1` (bin "day1") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Cool bear

It's clippy! We love clippy.

Right! And we don't need to link to its website, because it ships by default with rust.

$ rustup which cargo-clippy
/home/amos/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo-clippy

And it replaces/augments cargo check, which is what Visual Studio Code's "rust-analyzer" extension runs by default, but we can change that setting:

{
  "rust-analyzer.checkOnSave.command": "clippy",
}

(...which can also be changed in VS Code's graphical settings editor, by searching for "checkOnSave" or something).

Applying the suggestion, our code becomes this:

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let input = include_str!("input.txt");

    let mut lines = input.lines();
    while let Some(line) = lines.next() {
        println!("Got line: {}", line);
    }

    Ok(())
}

And then... another suggestion appears!

$ cargo clippy
warning: this loop could be written as a `for` loop
 --> src/main.rs:7:5
  |
7 |     while let Some(line) = lines.next() {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `for line in lines`
  |
  = note: `#[warn(clippy::while_let_on_iterator)]` on by default
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#while_let_on_iterator

warning: `day1` (bin "day1") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s

Fine, fine. In fact, let's inline everything:

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    for line in include_str!("input.txt").lines() {
        println!("Got line: {line}");
    }

    Ok(())
}
Cool bear

Wow hey, that almost looks like... idk, Python or something.

Yeah, except for the macros and ? to propagate errors and the fact that everything is strongly typed and..

Cool bear

Yes yeah okay, got it.

Splitting, grouping, and other -ings

So anyway, our program prints this:

$ cargo run --quiet
Got line: 1000
Got line: 2000
Got line: 3000
Got line:
Got line: 4000
Got line:
Got line: 5000
Got line: 6000
Got line:
Got line: 7000
Got line: 8000
Got line: 9000
Got line:
Got line: 10000

So, that's cool, it reads each line individually.

Now how about we try to group these, separated by an empty line?

Cool bear

Wait, couldn't we just split on \n\n?

How do you mean?

Cool bear

Well, our file is something like:

1000\n2000\n3000\n\n4000\n\n5000\n6000...

...right? If we just split on \n\n we have our groups?

Ah, you're right, let's try it:

    for group in include_str!("input.txt").split("\n\n") {
        println!("In group:");
        for line in group.lines() {
            println!("  - {line}");
        }
    }
$ cargo run --quiet
In group:
  - 1000
  - 2000
  - 3000
In group:
  - 4000
In group:
  - 5000
  - 6000
In group:
  - 7000
  - 8000
  - 9000
In group:
  - 10000
Cool bear

Great! So that's it then? We can finish solving the problem?

Not so fast bear! On Windows..

Cool bear

Yes yes, I know, on windows newlines aren't \n they're \r\n because teletypes, blah, who cares, here I'll fix it:

    for group in include_str!("input.txt")
        .replace("\r\n", "\n")
        .split("\n\n")
    {
        println!("In group:");
        for line in group.lines() {
            println!("  - {line}");
        }
    }
Cool bear

Boom, done. Can we move on?

Very well, if you insist — but we'll come back to my way later, right?

Cool bear

Yes, sure, whatever gets us the points for Advent of Code.

...you do realize we're late enough that we'll get 1 point no matter what?

Cool bear

SO THEN WHY ARE WE DO-

...ever hear of this new thing called fun?

Cool bear

Fun! Oh! Fun! Well excuse me, I hadn't noticed we were having fun.

Oh I don't know about you, but me, I've been having tons of fun.

Anyway sure, let's humor you and keep going.

So you want something real imperative huh? No more concepts?

Cool bear

Yes, enough with the concepts and the learning. Let's get straight to the point.

Very well. So, we'll want to parse every non-empty line as a number, right? If we're going to sum them?

Cool bear

Sure, okay, you can have that one.

So we must go from, like... &str to something like... let's say u64?

And we can do so like this:

    for group in include_str!("input.txt")
        .replace("\r\n", "\n")
        .split("\n\n")
    {
        println!("In group:");
        for line in group.lines() {
            let value = line.parse::<u64>()?;
            println!("  - {value}");
        }
    }

And that gives us:

$ cargo run --quiet
In group:
  - 1000
  - 2000
  - 3000
In group:
  - 4000
In group:
  - 5000
  - 6000
In group:
  - 7000
  - 8000
  - 9000
In group:
  - 10000

...that didn't change the output at all.

Cool bear

Yes yes, but now they're numbers. Sum them!

Okay fine, so, imperative style... I guess we make a sum "variable", set it to zero and add values as we go:

    for group in include_str!("input.txt")
        .replace("\r\n", "\n")
        .split("\n\n")
    {
        let mut sum = 0;
        for line in group.lines() {
            let value = line.parse::<u64>()?;
            sum += value;
        }
        println!("Group has sum {sum}");
    }
$ cargo run --quiet
Group has sum 6000
Group has sum 4000
Group has sum 11000
Group has sum 24000
Group has sum 10000
Cool bear

Yes, yes, good, now find the biggest one.

Well... again imperative style, I guess we can make a largest_sum variable and overwrite it if we find a bigger sum...

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let mut max = 0;
    for group in include_str!("input.txt")
        .replace("\r\n", "\n")
        .split("\n\n")
    {
        let mut sum = 0;
        for line in group.lines() {
            let value = line.parse::<u64>()?;
            sum += value;
        }
        if sum > max {
            max = sum;
        }
    }
    println!("The burdenedst elf is carrying {max} calories");

    Ok(())
}

Please note that even though every binding here is strongly typed, we don't have to specify the type of max, because..

Cool bear

Yes yes, the compiler infers it from usage because we add value to it, which is an u64 because we used a turbofish to specify the type parameter F of parse, excellent, let's run it.

$ cargo run --quiet
The burdenedst elf is carrying 24000 calories
Cool bear

There, we're done, goodbye!

Not so fast bear! First of all, that's not even our real puzzle input. And secondly, you promised we'd give my way a try.

Cool bear

Fine.

Solving it the fun way

So let's go back to this version of the code:

    for line in include_str!("input.txt").lines() {
        println!("{line}");
    }

What I personally would like to do here is to split that iterator into "groups" every time we encounter an empty line.

And this is surprisingly tricky to do!

We can collect all lines into a vector, and then, there's a bunch of interesting methods on slices, split for example:

    let lines = include_str!("input.txt").lines().collect::<Vec<_>>();
    let groups = lines.split(|line| line.is_empty()).collect::<Vec<_>>();
    println!("groups = {groups:?}");
$ cargo run --quiet
groups = [["1000", "2000", "3000"], ["4000"], ["5000", "6000"], ["7000", "8000", "9000"], ["10000"]]

See, this is a perfectly decent start!

    let lines = include_str!("input.txt").lines().collect::<Vec<_>>();
    let groups = lines.split(|line| line.is_empty()).collect::<Vec<_>>();

    let groups = groups
        .into_iter()
        .map(|group| {
            group
                .iter()
                .map(|v| v.parse::<u64>().ok())
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>();
    println!("groups = {groups:?}");
$ cargo run --quiet
groups = [[Some(1000), Some(2000), Some(3000)], [Some(4000)], [Some(5000), Some(6000)], [Some(7000), Some(8000), Some(9000)], [Some(10000)]]

clippy suggests we don't need that middle collect, and it is of course, correct — in fact we can inline the whole thing:

    let lines = include_str!("input.txt").lines().collect::<Vec<_>>();
    let groups = lines
        .split(|line| line.is_empty())
        .map(|group| {
            group
                .iter()
                .map(|v| v.parse::<u64>().ok())
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>();
    println!("groups = {groups:?}");

But... it might be easier to do the "parsing as u64" bit before we split:

    let lines = include_str!("input.txt")
        .lines()
        .map(|v| v.parse::<u64>().ok())
        .collect::<Vec<_>>();
    let groups = lines.split(|line| line.is_some()).collect::<Vec<_>>();
    println!("groups = {groups:?}");
$ cargo run --quiet
groups = [[], [], [], [None], [None], [], [None], [], [], [None], []]

Woops, wrong way round, let's try this instead:

//                           was: `is_some` 👇
    let groups = lines.split(|line| line.is_none()).collect::<Vec<_>>();
$ cargo run --quiet
groups = [[Some(1000), Some(2000), Some(3000)], [Some(4000)], [Some(5000), Some(6000)], [Some(7000), Some(8000), Some(9000)], [Some(10000)]]

Now, for each group, we can sum all elements. They're Option<u64> right now, we can use Option::unwrap to turn them into u64 or panic (similar to what Result::unwrap does, which we've used a bunch earlier).

Our code becomes:

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let lines = include_str!("input.txt")
        .lines()
        .map(|v| v.parse::<u64>().ok())
        .collect::<Vec<_>>();
    let groups = lines
        .split(|line| line.is_none())
        .map(|group| group.iter().map(|v| v.unwrap()).sum::<u64>())
        .collect::<Vec<_>>();
    println!("groups = {groups:?}");

    Ok(())
}
$ cargo run --quiet
groups = [6000, 4000, 11000, 24000, 10000]

And we can find the maximum here without even collecting the groups into their own Vec<u64>:

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let lines = include_str!("input.txt")
        .lines()
        .map(|v| v.parse::<u64>().ok())
        .collect::<Vec<_>>();
    let elven_lead = lines
        .split(|line| line.is_none())
        .map(|group| group.iter().map(|v| v.unwrap()).sum::<u64>())
        // 👇
        .max();
    println!("{elven_lead:?}");

    Ok(())
}
$ cargo run --quiet
Some(24000)

Why does max() return an Option<T> you ask?

Cool bear

Nope, nobody asked.

Oh you're still here. Well it's because what's the maximum of an empty iterator? It's not zero, or i64::min, or negative infinity: it's... nothing. There's no maximum then. Hence, None, or Some(maximum).

I'm fairly happy with that solution... but not totally.

I still think that collect in the middle is unnecessary. I'd rather we do everything with iterators, so that we could work on very large datasets without memory usage going up.

Cool bear

Don't we bake the whole input into the executable anyway?

..yes, right, but it's mapped in memory and... because virtual memory... things are paged in and out and... uhh it all works out.

Cool bear

Uh-huh. Eloquent as ever. Continue.

We could, of course, implement our own iterator type:

/// An iterator that takes `Option<u64>` items and yields sums of groups of
/// `Some(u64)` items separated by `None` items.
struct GroupSumIter<I> {
    inner: I,
}

impl<I> Iterator for GroupSumIter<I>
where
    I: Iterator<Item = Option<u64>>,
{
    type Item = u64;

    fn next(&mut self) -> Option<Self::Item> {
        let mut sum = loop {
            match self.inner.next() {
                Some(Some(v)) => break v,
                Some(None) => {
                    // huh, weird, didn't expect a separator there
                    // but let's just skip it
                }
                // we've reached the end of the inner iterator
                None => return None,
            }
        };

        loop {
            match self.inner.next() {
                Some(Some(v)) => sum += v,
                Some(None) | None => {
                    // reached a separator or the end of the iterator
                    break Some(sum);
                }
            }
        }
    }
}

Which we could use like this:

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let lines = include_str!("input.txt")
        .lines()
        .map(|v| v.parse::<u64>().ok());
    // here! 👋
    let elven_lead = GroupSumIter { inner: lines }.max();
    println!("{elven_lead:?}");

    Ok(())
}

And that does work!

$ cargo run --quiet
Some(24000)

I'm afraid we're giving Cool Bear ammunition to nominate this solution as "most convoluted" though.

Cool bear

Yes. Yes indeed.

So let's try and achieve this in less code, shall we?

Whenever we want to do "advanced iterator stuff", the same crate always pops up: itertools.

So, let's try it:

$ cargo add itertools
    Updating crates.io index
      Adding itertools v0.10.5 to dependencies.
             Features:
             + use_alloc
             + use_std

There's two methods I want to try. First, Itertools::batching, which lets us do the same, but without having to declare our own type.

// 👋 this is important, it lets us call `.batching` on iterators
use itertools::Itertools;

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let max = include_str!("input.txt")
        .lines()
        .map(|v| v.parse::<u64>().ok())
        .batching(|it| {
            let mut sum = None;
            while let Some(Some(v)) = it.next() {
                sum = Some(sum.unwrap_or(0) + v);
            }
            sum
        })
        .max();
    println!("{max:?}");

    Ok(())
}

This works!

$ cargo run --quiet
Some(24000)

And honestly, I'd be happy with that as my solution.

I'm curious about Itertools::coalesce though, let's give it a shot:

use itertools::Itertools;

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let max = include_str!("input.txt")
        .lines()
        .map(|v| v.parse::<u64>().ok())
        .coalesce(|a, b| match (a, b) {
            (None, None) => Ok(None),
            (None, Some(b)) => Ok(Some(b)),
            (Some(a), Some(b)) => Ok(Some(a + b)),
            (Some(a), None) => Err((Some(a), None)),
        })
        .max()
        .flatten()
        .unwrap_or_default();
    println!("{max:?}");

    Ok(())
}
$ cargo run --quiet
24000

This is... arguably worse than the the other solution, but it's also hilarious, so it's impossible to tell if it's good or not.

Also, it's easier to follow with inlay hints, as usual:

The same code as above, but as VS Code screenshots. It even shows closure return types, something that is off by default I believe

Anyway, let's try it with the real puzzle input (which is 2248 lines long).

We'll make a release build:

$ cargo build --release
(omitted)

And time it with hyperfine:

$ hyperfine ./target/release/day1
Benchmark 1: ./target/release/day1
  Time (mean ± σ):       0.2 ms ±   0.4 ms    [User: 0.3 ms, System: 0.1 ms]
  Range (min … max):     0.0 ms …  13.9 ms    3081 runs

  Warning: Command took less than 5 ms to complete. Note that the results might be inaccurate because hyperfine can not calibrate the shell startup time much more precise than this limit. You can try to use the `-N`/`--shell=none` option to disable the shell completely.
  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet PC without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.

Ah, well. Too fast to measure properly 🤷

Part 2

Oh, there's a part a two, forgot about that part. In part two, instead of finding the single buffest elf, we must find the top 3 (and compute the sum of the calories they're carrying).

Well it almost feels like cheating, but once again itertools has everything we need.

We can, first of all, flatten our Iterator<Item = Option<u64>> into a Iterator<Item = u64> with Iterator::flatten. This works because Option<u64> itself implements IntoIterator: it iterates over one item if we have the Some(T) variant, or over.. zero items if we have the None variant.

Then, we can sort it. Naively, it'll sort from smallest to largest. But if we give it u64::MAX - v instead of v, it'll sort from largest to smallest! We could've also called .rev() to reverse the iterator after sorting it, but that sounds more expensive for some reason.

Cool bear

It's low-key expensive in both cases tbh, since it needs to keep track of all elves rather than just the top 3 at any given time.

I mean, expensive is relative since this fits a gazillion times over in main memory, but, y'know.

Then we can take 3 items from the resulting iterator (elves sorted by calories carried, most to least) with take(3), and sum that!

And there we have it:

use itertools::Itertools;

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let answer = include_str!("input.txt")
        .lines()
        .map(|v| v.parse::<u64>().ok())
        .coalesce(|a, b| match (a, b) {
            (None, None) => Ok(None),
            (None, Some(b)) => Ok(Some(b)),
            (Some(a), Some(b)) => Ok(Some(a + b)),
            (Some(a), None) => Err((Some(a), None)),
        })
        .flatten()
        .sorted_by_key(|&v| u64::MAX - v)
        .take(3)
        .sum::<u64>();
    println!("{answer:?}");

    Ok(())
}

Again, coalesce here is a bit on the odd side, I still think batching worked better, but ah well.

Reader suggestion: std::cmp::Reverse

Instead of doing u64::MAX - v, we can use Reverse.

This line:

        .sorted_by_key(|&v| u64::MAX - v)

becomes:

        .sorted_by_key(|&v| std::cmp::Reverse(v))

...which I think expresses intent better! Thanks to freax13 on Reddit for this suggestion.

Reader suggestion: k_smallest

crazy01010 suggests on Reddit that we can use itertool's k_smallest function in conjunction with Reverse to find the "K largest values", which is what we want.

We can combine this with our batching solution and a suggestion of my own: the for loop we had here:

        .batching(|it| {
            let mut sum = None;
            while let Some(Some(v)) = it.next() {
                sum = Some(sum.unwrap_or(0) + v);
            }
            sum
        })

...looks like a potential use case for itertool's fold_while.

The complete solution becomes:

use itertools::{FoldWhile, Itertools};
use std::cmp::Reverse;

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let answer = include_str!("input.txt")
        .lines()
        .map(|v| v.parse::<u64>().ok())
        // consider all lines separated by `None`
        .batching(|it| {
            it.fold_while(None, |acc: Option<u64>, v| match v {
                Some(v) => FoldWhile::Continue(Some(acc.unwrap_or_default() + v)),
                // that's the group separator, `fold_while` is done
                None => FoldWhile::Done(acc),
            })
            // this returns `Some(total)` if we found a group, `None` if we're
            // at the end, to let `batching` end.
            .into_inner()
        })
        // this turns `k_smallest` into `k_largest`
        .map(Reverse)
        .k_smallest(3)
        // and this strips the `Reverse` so we can sum
        .map(|x| x.0)
        .sum::<u64>();
    println!("{answer:?}");

    Ok(())
}

And it works beautifully!

Reader suggestions: using a BinaryHeap

Another suggestion by crazy01010 on Reddit: we can use a BinaryHeap to maintain the sums in order: we push the first three, and any time we push another one, we also pop from the heap.

That way, the heap never grows beyond 4 items, and thanks to std::cmp::Reverse, we get the three largest items (instead of the three smallest, if we used the heap naively).

use itertools::{FoldWhile, Itertools};
use std::{cmp::Reverse, collections::BinaryHeap};

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let mut group_sums = include_str!("input.txt")
        .lines()
        .map(|v| v.parse::<u64>().ok())
        .batching(|it| {
            it.fold_while(None, |acc: Option<u64>, v| match v {
                Some(v) => FoldWhile::Continue(Some(acc.unwrap_or_default() + v)),
                None => FoldWhile::Done(acc),
            })
            .into_inner()
        })
        .map(Reverse);

    let mut heap = BinaryHeap::new();

    for init in (&mut group_sums).take(3) {
        heap.push(init);
    }

    for rest in group_sums {
        heap.push(rest);
        heap.pop();
    }

    let answer = heap.into_iter().map(|Reverse(v)| v).sum::<u64>();
    println!("{answer:?}");

    Ok(())
}

I learned something while implementing this solution: I was initially doing group_sums.take(3) but it consumed the iterator. Since a mutable reference also implements Iterator, we can simply do (&mut group_sums).take(3).

Improving the batching solution

This gave me an idea to improve the batching solution further, with map_while and sum1:

use itertools::Itertools;
use std::cmp::Reverse;

fn main() {
    let answer = include_str!("input.txt")
        .lines()
        .map(|v| v.parse::<u64>().ok())
        .batching(|it| it.map_while(|x| x).sum1::<u64>())
        .map(Reverse)
        .k_smallest(3)
        .map(|x| x.0)
        .sum::<u64>();
    println!("{answer:?}");
}

Look how pretty!

(Thanks to Hodkinson on GitHub for pointing out this version of the code doesn't need "mut sprinkling". It doesn't!)

Comment on /r/fasterthanlime

(JavaScript is required to see this. Or maybe my stuff broke)

Here's another article just for you:

Aiming for correctness with types

The Nature weekly journal of science was first published in 1869. And after one and a half century, it has finally completed one cycle of carcinization, by publishing an article about the Rust programming language.

It's a really good article.

What I liked about this article is that it didn't just talk about performance, or even just memory safety - it also talked about correctness.