【译】用 Rust 实现 Advent of Code 2020 第1天

267 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

今年 12 月我还没有具体的计划,但是我周围的很多人(在 Twitter 上,在工作中)都选择了这个 Advent of Code 来学习 Rust,而且我有很大的 FOMO 情绪,所以,看看最后我能走多远。

我将在 Linux 上完成这一系列作品,因此可能会涉及到一些命令行工具,但不要担心 —— 代码理论上能在所有平台上运行。

如果你想继续,你需要安装带有 Rust Analyzer 扩展的 VS Code,并且你还需要安装 Rust

在 Windows 上,你可能必须安装 VS2019 构建工具,这可能有点烦人 - 抱歉!

第一部分

问题的输入类似下方

1721
979
366
299
675
1456

我们的任务是找到两数相加的和为 2020 的项,然后把它们相乘,让我们开始吧!

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

首先,让我将题目输入粘贴到 day-1/src/input.txt 文件中:

1470
1577
1054
(cut)
1911
1282
1306

但你的文件名可能跟我不一样

然后,我们要读取这个文件,这里有几个选项,我们可以在运行时处理:

$ cargo add anyhow
    Updating 'https://github.com/rust-lang/crates.io-index' index
      Adding anyhow v1.0.35 to dependencies

小贴士: cargo addcargo-edit 提供,我们将在整个系列中使用这个插件。 如果你按照以下步骤,可以简单地安装它并编辑依赖文件 cargo install cargo-edit

// in `day1/src/main.rs`

fn main() -> anyhow::Result<()> {
    let s = std::fs::read_to_string("./src/input.txt")?;
    dbg!(&s[..40]);

    Ok(())
}

这将读取文件,并打印前 40 个字节内容:

$ cargo run --quiet
[src/main.rs:3] &s[..40] = "1470\n1577\n1054\n1962\n1107\n1123\n1683\n1680\n"

... 但是在运行时打开文件可能会失败,我们需要确保 input.txt 文件总是在可执行文件的旁边。

因此,我们只需要在编译时将它包含进去:

fn main() -> anyhow::Result<()> {
    let s = include_str!("input.txt");
    dbg!(&s[..40]);

    Ok(())
}
$ cargo run --quiet
[src/main.rs:3] &s[..40] = "1470\n1577\n1054\n1962\n1107\n1123\n1683\n1680\n"

现在字符串内容是可执行文件的一部分:

$ xxd target/debug/day1 | grep "1470.15" -A 5
0003b000: 4572 726f 723a 200a 3134 3730 0a31 3537  Error: .1470.157
0003b010: 370a 3130 3534 0a31 3936 320a 3131 3037  7.1054.1962.1107
0003b020: 0a31 3132 330a 3136 3833 0a31 3638 300a  .1123.1683.1680.
0003b030: 3131 3736 0a31 3931 370a 3137 3836 0a31  1176.1917.1786.1
0003b040: 3536 350a 3134 3634 0a31 3039 370a 3133  565.1464.1097.13
0003b050: 3633 0a31 3039 310a 3130 3732 0a31 3832  63.1091.1072.182

小贴士: xxd 是大多数 Linux 发行版附带的一个非常基本(且古老)的 hexdump 工具。

到目前为止,我们有一个很大的 String。我们希望分别处理每一行,所以,让我们用换行符对其分割:

fn main() -> anyhow::Result<()> {
    let s = include_str!("input.txt").split("\n");

    Ok(())
}

小贴士: 你可能想用 .lines() 代替 .split("\n") 进行拆分,以便这与 CRLF 行结束兼容(Windows)。本文的其余部分是按照 split 的思路编写的,但是它们都提供了迭代器,所以你可以随便选。

它给我们的不是一个字符串数组,而是一个实现了 Iterator<Item = &str> 的具体类型(Split<&str>)。

酷熊:等等,&str?我们不是有 String 吗?

Amos: 的确如此!我们的迭代器现在返回的元素是从原来的 String 中借来的。它们是来自原始 String 的片段,因此不涉及复制。

酷熊:等等,不!当我们使用 std::fs::read_to_string 时,我们确实有 String:

但是等等... ... 当我们使用 include_str! 时,我们得到的是 &str:

Amos: 哦,对了!当我们“将字符串打包进在可执行文件中”时,我们得到的也是借用的字符串,因此,我们只是从可执行文件中借用字符串。

酷熊:“可执行文件”是指... ? (要问一个朋友)

Amos: target/debug/day1 中的文件是 Linux 上的 ELF、 Windows 上的 PE 和 macOS 上的 Mach-O,这是编译程序得到的结果(通过 cargo build 或 cargo run 构建,最终都会调用 rustc 编译器)。

酷熊:对!

因此,我们有 Split<&str>,且它实现了 Iterator<Item = &str>

Iterator 是一个 trait,最重要的必需方法是 next()

fn next(&mut self) -> Option<Self::Item>

所以如果我们调用 s.next(),我们将得到一个 Some(a_slice),或者在元素耗尽时返回 None

fn main() -> anyhow::Result<()> {
    let mut s = include_str!("input.txt").split("\n");
    dbg!(s.next());
    dbg!(s.next());
    dbg!(s.next());

    Ok(())
}
$ cargo run --quiet
[src/main.rs:3] s.next() = Some(
    "1470",
)
[src/main.rs:4] s.next() = Some(
    "1577",
)
[src/main.rs:5] s.next() = Some(
    "1054",
)

酷熊:等等,我收到一个警告:

Amos: 你遇到内联错误的警告?

酷熊:是的,这是 vscode 扩展 Error Lens,它相当整洁!

好吧,关于那个错误 —— 它不是一个真正的错误,它是一个来自 clippy 的诊断提示,这...我之前说过你应该安装 clippy 吧?

那么,你应该将它作为 rust-analyzer 默认的“检查保存”命令,将其保存在你的 VSCode 用户设置中:

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

clippy 试图告诉我们的是 '\n' 只是一个字符,这是一个很小的值,用其分割会被高度优化,而 "\n" 是一个字符串,可以是任意长度(只不过这里它的长度恰好是 1),所以我们强迫 .split 使用更通用(会导致更慢)的方法。

没有问题,我们可以很容易地修复,通过点击编辑器中的灯泡(💡)图标,或只是按下 Ctrl + .(MacOS 上可能是 Cmd + .):

然后我们得到下面的代码:

fn main() -> anyhow::Result<()> {
    let mut s = include_str!("input.txt").split('\n');
    dbg!(s.next());
    dbg!(s.next());
    dbg!(s.next());

    Ok(())
}

执行结果和之前一样。

那么... 接下来我们要做什么呢? 让我们再看一下问题陈述:

具体来说,它们需要你找到两个总和为 2020 的条目,然后将这两个数字相乘。

对!就是描述的那样,不过有一个问题 —— 我们还没有数值,只有字符串。

幸运的是,str::parse 可以做到这一点!

那么,我们从一个迭代器 Iterator<Item = &str> 开始,我们应该需要 ... Iterator<Item = i64>

小贴士 i64 是一个有符号的 64 位整数——当我们不担心内存使用问题并且不确定能得到多大的数字时,使用它似乎是一个相对安全的选择。

对迭代器的所有项进行转换称为 mapping。由 Iterator trait 提供的 .map 方法可以实现 mapping。

我们可以直接传递一个函数: 在这里,我们传递 str::parse

fn main() -> anyhow::Result<()> {
    let mut s = include_str!("input.txt").split('\n').map(str::parse);
    dbg!(s.next());
    dbg!(s.next());
    dbg!(s.next());

    Ok(())
}

似乎不管用,报错了:

$ cargo run --quiet
error[E0283]: type annotations needed for `Map<std::str::Split<'_, char>, for<'r> fn(&'r str) -> std::result::Result<F, <F as FromStr>::Err> {core::str::<impl str>::parse::<F>}>`
    --> src/main.rs:2:59
     |
2    |     let mut s = include_str!("input.txt").split('\n').map(str::parse);
     |         -----                                             ^^^^^^^^^^ cannot infer type for type parameter `F` declared on the associated function `parse`
     |         |
     |         consider giving `s` the explicit type `Map<std::str::Split<'_, char>, for<'r> fn(&'r str) -> std::result::Result<F, <F as FromStr>::Err> {core::str::<impl str>::parse::<F>}>`, where the type parameter `F` is specified
     | 
    ::: /home/amos/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/str/mod.rs:2202:21
     |
2202 |     pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> {
     |                     ------- required by this bound in `core::str::<impl str>::parse`
     |
     = note: cannot satisfy `_: FromStr`
help: consider specifying the type argument in the function call
     |
2    |     let mut s = include_str!("input.txt").split('\n').map(str::parse::<F>);
     |                                                                     ^^^^^

这绝对是个大错误,太不可思议了。

不过在最后还是有点帮助信息:

help: consider specifying the type argument in the function call
     |
2    |     let mut s = include_str!("input.txt").split('\n').map(str::parse::<F>);
     |                                                                     ^^^^^

你看,str::parse 可以把一个字符串解析成许多不同的东西 —— 我们可以解析一个 IP 地址,比如 127.0.0.1,或者我们可以解析成一个浮点值,比如 3.1415926535,诸如此类。

因此,借助我们的朋友 turbo-fish 的一点帮助,我们可以明确地说: 我们想要解析的是一个 i64 —— 一个有符号的 64 位整数:

fn main() -> anyhow::Result<()> {
    let mut s = include_str!("input.txt").split('\n').map(str::parse::<i64>);
    dbg!(s.next());
    dbg!(s.next());
    dbg!(s.next());

    Ok(())
}

然后它就能正常执行了:

$ cargo run --quiet
[src/main.rs:3] s.next() = Some(
    Ok(
        1470,
    ),
)
[src/main.rs:4] s.next() = Some(
    Ok(
        1577,
    ),
)
[src/main.rs:5] s.next() = Some(
    Ok(
        1054,
    ),
)

我们所做的与下面的 JavaScript 代码类似,但并不完全相同:

["12", "34", "56"].map((x) => parseInt(x, 10))

酷熊小贴士 通过 JavaScript 代码可知: parseInt 接受多个参数,包括要解析的数字的基数(radix),因此我们不能只是将其传递给 map,后者将传递项和索引。 这个方法有效:

> ["12", "34", "56"].map((x) => parseInt(x, 10))
[ 12, 34, 56 ]

但下面这种方式就不行:

> ["12", "34", "56"].map(parseInt)
[ 12, NaN, NaN ]

它只对第一个(索引 0)和第十一个(索引 10)元素正常工作,否则它将尝试以 1 为基数、以 2 为基数、以 3 为基数等 11 个元素进行解析。 JavaScript 很有趣吧?

因为在 JavaScript 中,map 操作一个数组并返回一个数组。

但是在这里,我们使用迭代器: item 流。我们还不能随机访问他们所以我们必须打反复调用 .next() ,并且直到它们返回 None

事实上,如果我们仔细观察我们的输出,我们会注意到我们每次打印的内容实际上是一个 Option<Result<i64, E>>

[src/main.rs:3] s.next() = Some(
    Ok(
        1470,
    ),
)

小贴士 Option<T> 可以是 Some(T) ,也可以是 None —— iter.next() 当没有item 时将返回 NoneResult<T, E> 可以是 Ok(T)Err(E) —— 题目中输入的所有的行都是数字,所以我们只会得到 Ok(i64)

那么,我们怎样才能得到“一组数字”呢?

如果我们在 s.next() 的结果上调用 .unwrap() 的话,我们将从 Option<Result<i64, E>> 转换为 Result<i64, E>

fn main() -> anyhow::Result<()> {
    let mut s = include_str!("input.txt").split('\n').map(str::parse::<i64>);
    dbg!(s.next().unwrap());
    dbg!(s.next().unwrap());
    dbg!(s.next().unwrap());

    Ok(())
}
cargo run --quiet
[src/main.rs:5] s.next().unwrap() = Ok(
    1470,
)
[src/main.rs:6] s.next().unwrap() = Ok(
    1577,
)
[src/main.rs:7] s.next().unwrap() = Ok(
    1054,
)

如果我们再次调用 .unwrap(),那么我们会得到单纯的 i64 值,如下所示:

fn main() -> anyhow::Result<()> {
    let mut s = include_str!("input.txt").split('\n').map(str::parse::<i64>);
    dbg!(s.next().unwrap().unwrap());
    dbg!(s.next().unwrap().unwrap());
    dbg!(s.next().unwrap().unwrap());

    Ok(())
}
cargo run --quiet
[src/main.rs:3] s.next().unwrap().unwrap() = 1470
[src/main.rs:4] s.next().unwrap().unwrap() = 1577
[src/main.rs:5] s.next().unwrap().unwrap() = 1054

酷熊:就差一点! 但是... 这里到底发生了什么? Amos: 当调用 Option<T>::unwrap() 要么 panic(如果它遇到了 None 变体),要么返回一个 T

酷熊:panic,这不是很糟吗?

Amos: 没那么糟啦!在我们的程序中,如果其中一行不是数字,我们就不能做任何有用的事情,所以我们不妨安全地停止程序 —— 也就是恐慌(panic)。

酷熊:有道理,那第二次 unwrap() 呢?

Amos: 这个实际上是 Result<T, E>::unwrap() —— 它的工作原理也是类似。如果遇到 Err,它会 panic(携带错误类型 E 的格式化消息)。否则,它也返回 T

酷熊:好吧。快速提问: 我们是否可以将 unwrap 传递给 map,这样我们就可以在从迭代器中检索所有项目时执行 unwrap?

Amos: 是的,我们可以! 让我们试试。

fn main() -> anyhow::Result<()> {
    let mut s = include_str!("input.txt")
        .split('\n')
        .map(str::parse::<i64>)
        .map(Result::unwrap);
    dbg!(s.next().unwrap());
    dbg!(s.next().unwrap());
    dbg!(s.next().unwrap());

    Ok(())
}
$ cargo run --quiet
[src/main.rs:6] s.next().unwrap() = 1470
[src/main.rs:7] s.next().unwrap() = 1577
[src/main.rs:8] s.next().unwrap() = 1054

酷熊:好的,那么...现在记笔记, s 是一个 Iterator<Item = i64>,对吗?

Amos: 对!

酷熊:那我们为什么还要 unwrap 呢?

Amos: 嗯,因为我们可以消耗完所有的 item —— 此时调用 .next() 仍然返回 Option<i64>

酷熊:你能告诉我这时候会发生什么吗?

fn main() -> anyhow::Result<()> {
    let mut s = include_str!("input.txt")
        .split('\n')
        .map(str::parse::<i64>)
        .map(Result::unwrap)
        .skip(198); // new!

    dbg!(s.next().unwrap());
    dbg!(s.next().unwrap());
    dbg!(s.next().unwrap());

    Ok(())
}
$ cargo run --quiet
[src/main.rs:8] s.next().unwrap() = 1282
[src/main.rs:9] s.next().unwrap() = 1306
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:10:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

酷熊:哦,对了,我们的 input.txt 文件只有 200 行,所以如果我们继续调用 .next()。在那之后,我们终于得到了 None。抓到你了。

我们先继续解题

酷熊:等等,抱歉打断你们。.如果我不想调用 Result::unwrap —— 如果我不想 panic,而是想返回一个简洁的错误,那该怎么办?

Amos: 我们过会儿再说这个,拉钩保证!

酷熊:好吧。

那么!问题是,我们需要在输入中找到一对数字,它们的总和是 2020,然后将它们相乘。

让我们尝试一些类型驱动的开发:

fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
    todo!()
}

因此,给定一个 Vec<i64> —— 可以说就像我们今天所说的“array”一样接近,这个函数应该尝试找到两个和为 2020 的数字,并将它们作为元组返回,如: (2019,1)。

然而,它不是简单地返回一个 (i64,i64),因为这样的一对完全有可能不存在!如果我们的输入只是 vec![1, 3, 5],好吧,两个数字的组合不会等于 2020。

在实现这个函数之前,让我们尝试弄清楚如何使用它。

到目前为止,我们只有一个 Iterator<Item = i64>。但是这个函数希望随机访问集合中的任意项,所以它只能是 Vec<i64>

Iterator::collect 可以帮助你完成工作。

main 函数如下:

fn main() -> anyhow::Result<()> {
    let pair = find_pair_whose_sum_is_2020(
        include_str!("input.txt")
            .split('\n')
            .map(str::parse::<i64>)
            .map(Result::unwrap)
            .collect(),
    );
    dbg!(pair);

    Ok(())
}

fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
    todo!()
}

编译后:

$ cargo check
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s

但它不能运行:

$ cargo run --quiet
thread 'main' panicked at 'not yet implemented', src/main.rs:17:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

酷熊:哦,todo!() 是等待我们去实现的吗?

Amos: 对!它允许我们将某些内容标记为“待完成”,并且它将进行编译(而不是抱怨我们没有返回正确的类型,如果我们让 find_pair_whose_sum_is_2020 函数体直接为空,编译器会直接报错)

酷熊:太棒了,但结尾为何是个 !

Amos: 因为它是,不是函数。

现在我们要做的就是实现这个函数!

Vec<i64> 可以视为一种数组,所以我们可以调用 .len(),用于返回它所拥有的条目数,类型是 usize,然后我们可以使用 [] 对它进行索引取值,就像在 JavaScript 中一样。

让我们试一试:

fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
    for i in 0..s.len() {
        for j in 0..s.len() {
            if i == j {
                continue;
            }
            if s[i] + s[j] == 2020 {
                return Some((s[i], s[j]));
            }
        }
    }
    None
}

酷熊:这是怎么回事? 嵌套循环?

Amos: 是的,我们需要输入所有可能的数字对。

酷熊:continue 是因为...?

Amos: 因为如果我们的第一个项是 1010,我们另一个项是其本身,导致和为 2020。配对必须由不同的元素组成。

酷熊:原来如此。

上面这个解决方案真的有效!

$ cargo run --quiet
[src/main.rs:11] pair = Some(
    (
        376,
        1644,
    ),
)

$ echo $((376 + 1644))
2020

酷熊:棒!可以改进吗?

Amos: 我想做一些改进。

首先,让我们实际上解决酷熊的合理要求“适当的错误处理”。

正如我们从 Iterator<Item = i64> 收集到的 Vec<i64> 一样,我们也可以将 Iterator<Item = Result<i64, E>> 收集到 Result<Vec<i64>, E> 中。

酷熊:所以这是一个不同的 collect 实现,它只是在出现第一个错误时停止?

Amos: 是的! 如果没有一个条目是 Err 变体,那么你将得到 Ok(some_vec),否则,将得到 Err(first_error)

我们试试:

fn main() -> anyhow::Result<()> {
    let pair = find_pair_whose_sum_is_2020(
        include_str!("input.txt")
            .split('\n')
            .map(str::parse::<i64>)
            .collect::<Result<Vec<_>, _>>()?,
    );
    dbg!(pair);

    Ok(())
}

(这段代码具有完全相同的输出 —— 我就不展示 shell 会话的执行结果了。)

酷熊:不错! 还有,那有条大 turbofish。 在 collect() 之后为何还有 ?

Amos: 在某种意义上,它有点类似于 unwrap(),在那个场景中,它返回的是 Result<T, E> 和 “return”(实际上是 “evaluates to”)T

酷熊:但它不会 panic 吗?

Amos: 它不会 panic,如果 Result 是 Err 变体,它返回一个 Err(E)

酷熊:啊,所以它只能在返回 Result<T, E> 的函数中工作?

Amos: 对!

酷熊:但是我们的函数返回类型是 anyhow::Result<()>,哪里有 E

Amos: 好吧,anyhow 是一个有助于错误处理的板条箱(crate)—— 它附带了一个错误类型,可以包含其他任何错误,真的。

因此,anyhow::Result 的定义实际上是:

pub type Result<T, E = Error> = core::result::Result<T, E>;

无论如何,这里的 Erroranyhow::Error

酷熊:好的,那么 Result<()> 中的 () 是什么意思呢?

Amos: 空元组!

看看我们的 find_pair_whose_sum_is_2020 函数是如何返回 Option<(i64, i64)> 的。

酷熊:那类型 (),大小是多少..。

Amos: 大小为零,没有开销。

好了,我们的代码没问题,我想:

fn main() -> anyhow::Result<()> {
    let pair = find_pair_whose_sum_is_2020(
        include_str!("input.txt")
            .split('\n')
            .map(str::parse::<i64>)
            .collect::<Result<Vec<_>, _>>()?,
    );
    dbg!(pair);

    Ok(())
}

fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
    for i in 0..s.len() {
        for j in 0..s.len() {
            if i == j {
                continue;
            }
            if s[i] + s[j] == 2020 {
                return Some((s[i], s[j]));
            }
        }
    }
    None
}

但我想给你们看更多的东西。

看,find_pair_whose_sum_is_2020 有点困扰我。你知道什么会很酷吗? 一个返回“结果对”的函数,像这样:

fn pairs(s: Vec<i64>) -> Vec<(i64, i64)> {
    todo!()
}

酷熊:嗯。我们真的需要在这里使用 Vec<i64> 吗?其他集合类型呢?例如,[i64; 12]类型如果使用这种方法会不生效吗?固定大小的数组?

Amos: 有可能,我们可以使用切片(slice)替代。

fn all_pairs(s: &[i64]) -> Vec<(i64, i64)> {
    todo!()
}

让我们真正实现它:

fn all_pairs(s: &[i64]) -> Vec<(i64, i64)> {
    let mut pairs: Vec<_> = Default::default();
    for i in 0..s.len() {
        for j in 0..s.len() {
            pairs.push((s[i], s[j]))
        }
    }
    pairs
}

然后我们可以在 find_pair_whose_sum_is_2020 中使用它:

fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
    for (a, b) in all_pairs(&s[..]) {
        if a + b == 2020 {
            return Some((a, b));
        }
    }
    None
}

那个版本仍然正常执行!

酷熊:不错,不过我在想... 如果我们早点找到结果对(pair)呢? 在这种情况下,如果我们只使用前几个结果对,那么计算“所有的对”不是很浪费吗?

Amos: 没错,有什么办法解决这个问题吗?

酷熊:那么,我们可以在 all_pairs 中不返回 Vec<(i64, i64)>,而是返回... Iterator<Item = (i64, i64)>

Amos: 可以! 事实上,你可以迭代一个 Vec,所以我们已经可以使用“迭代器风格”(iterator-style):

fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
    all_pairs(&s[..]).into_iter().find(|(a, b)| a + b == 2020)
}

酷熊:哇哦!即使我们一开始就“找到”总和是 2020 的结果对,我们仍然在构建整个 Vec

Amos: 没错,我们把这个也修复一下。

fn all_pairs(s: &[i64]) -> impl Iterator<Item = (i64, i64)> + '_ {
    s.iter()
        .copied()
        .enumerate()
        .map(move |(a_index, a)| {
            s.iter()
                .copied()
                .enumerate()
                .filter_map(move |(b_index, b)| {
                    if a_index == b_index {
                        None
                    } else {
                        Some((a, b))
                    }
                })
        })
        .flatten()
}

酷熊:这... 有点粗糙。 我们能换种方式写吗?

Amos: 可以! 当然可以。或者我们仅仅使用 itertools crate 就能做到。

$ cargo add itertools
      Adding itertools v0.9.0 to dependencies

实现变成如下所示:

use itertools::Itertools;

fn main() -> anyhow::Result<()> {
    let pair = find_pair_whose_sum_is_2020(
        include_str!("input.txt")
            .split('\n')
            .map(str::parse::<i64>)
            .collect::<Result<Vec<_>, _>>()?,
    );
    dbg!(pair);

    Ok(())
}

fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
    s.into_iter()
        .tuple_combinations()
        .find(|(a, b)| a + b == 2020)
}

酷熊:哇,这样的话,我们是否有必要将 find_pair_whose_sum_is_2020 作为一个单独的函数?

Amos: 没有必要了。

use itertools::Itertools;

fn main() -> anyhow::Result<()> {
    let (a, b) = include_str!("input.txt")
        .split('\n')
        .map(str::parse::<i64>)
        .collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .tuple_combinations()
        .find(|(a, b)| a + b == 2020)
        .expect("no pair had a sum of 2020");

    dbg!(a + b);
    dbg!(a * b);

    Ok(())
}
$ cargo run --quiet
[src/main.rs:13] a + b = 2020
[src/main.rs:14] a * b = 618144

酷熊:太棒了! 我们为什么需要那个 .collect?我知道我们需要处理错误,但是我们不能只使用像 itertools::process_result 这样的工具吧?

Amos: 不能,因为 tuple_combinations 需要迭代器实现了 Clone

酷熊:啊,因为它在输入中迭代好几次来组成组合..。

Amos: 你无法只用一次迭代来完成这件事,因为一旦你检索到一个值,它就会从迭代器中移除。

酷熊:第一部分结束了吗? Amos: 希望如此!

第二部分

我们看看问题陈述的第二部分:

在你的费用报告中,三项和为 2020 的所有组合有哪些?

酷熊:等等,等等,我可以做!

use itertools::Itertools;

fn main() -> anyhow::Result<()> {
    let (a, b, c) = include_str!("input.txt")
        .split('\n')
        .map(str::parse::<i64>)
        .collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .tuple_combinations()
        .find(|(a, b, c)| a + b + c == 2020)
        .expect("no tuple of length 3 had a sum of 2020");

    dbg!(a + b + c);
    dbg!(a * b * c);

    Ok(())
}
$ cargo run --quiet
[src/main.rs:13] a + b + c = 2020
[src/main.rs:14] a * b * c = 173538720

Amos: 干得好,小熊! 很幸运我们有 itertools 库。

酷熊:再见!

——完——