【问题标题】:How do I write a lazily evaluated double for loop in a functional style?如何以函数式风格编写一个惰性求值的 double for 循环?
【发布时间】:2021-03-23 07:01:05
【问题描述】:

如何在 Rust 中以函数式风格编写一个惰性求值的 double for 循环?

借来的值是 usize 类型,应该可以很容易地复制。

fn main() {
    let numbers: Vec<i32> = (1..100).collect();
    let len = numbers.len();

    let _sums_of_pairs: Vec<_> = (0..len)
        .map(|j| ((j + 1)..len).map(|k| numbers[j] + numbers[k]))
        .flatten()
        .collect();
}
error[E0373]: closure may outlive the current function, but it borrows `j`, which is owned by the current function
 --> src/bin/example.rs:6:37
  |
6 |         .map(|j| ((j + 1)..len).map(|k| numbers[j] + numbers[k]))
  |                                     ^^^         - `j` is borrowed here
  |                                     |
  |                                     may outlive borrowed value `j`
  |
note: closure is returned here
 --> src/bin/example.rs:6:18
  |
6 |         .map(|j| ((j + 1)..len).map(|k| numbers[j] + numbers[k]))
  |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `j` (and any other referenced variables), use the `move` keyword
  |
6 |         .map(|j| ((j + 1)..len).map(move |k| numbers[j] + numbers[k]))
  |                                     ^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0373`.

补充说明

  • 我知道Itertools::combinations(2) 可以完成这项工作。但是,我不想使用它,因为(1)我想知道如何自己做,(2)这可能是我的代码很慢的原因,我想消除那个来源。 (更新:Itertools::tuple_combinations&lt;(_, _)&gt;() 的速度要快得多,而且可以让你以函数式的方式编写代码。)
  • 我也试过先把它收集到一个容器里。 (0..len).collect::&lt;Vec&lt;_&gt;&gt;().iter().cloned().map(...)
  • 我尝试了建议的move,但随后numbers 也被移动,因此在下一个循环中不可用。
  • 在此代码示例中的任何地方都没有发生线程或异步。
  • Shepmaster 在this 回答中说我无法对闭包进行生命周期注释。
  • 我不写两个提前返回的原始循环的原因是,如果我想说,运行.any() 来查找是否存在特定值,我必须将两个循环移动到一个单独的函数,因为我不能将 return true; 放入循环中,除非它位于单独的函数中。

【问题讨论】:

  • 你需要使用移动闭包:move |k| numbers[j] + numbers[k]
  • “我尝试了建议的移动,但数字也被移动了,因此在下一个循环中不可用。” ...我得到的错误是:“无法移出numbersFnMut 闭包中的捕获变量”
  • 附带说明,.map(...).flatten() 可以简化为 .flat_map()

标签: rust closures ownership


【解决方案1】:

要解决此问题,您可以先借用&amp;numbers,然后直接使用numbers。然后你可以在第二个闭包中添加move

fn main() {
    let numbers: Vec<i32> = (1..100).collect();
    let len = numbers.len();

    let numbers = &numbers;

    let _sums_of_pairs: Vec<_> = (0..len)
        .map(|j| ((j + 1)..len).map(move |k| numbers[j] + numbers[k]))
        .flatten()
        .collect();
}

【讨论】:

    【解决方案2】:

    我建议您使用迭代器而不是索引来编写更惯用的 Rust 代码。它只是更简单。

    fn main() {
        let numbers: Vec<i32> = (1..100).collect();
    
        let _sums_of_pairs: Vec<_> = numbers.iter()
            .enumerate()
            .flat_map(|(j, &num)| numbers[j + 1..].iter().map(move |&k| k + num))
            .collect();
    }
    

    如果您仍想使用索引(请注意效率要低得多),这将起作用:

    fn main() {
        let numbers: Vec<i32> = (1..100).collect();
        let len = numbers.len();
    
        let _sums_of_pairs: Vec<_> = (0..len)
            .flat_map(|j| {
                let numbers = &numbers;
                ((j + 1)..len).map(move |k| numbers[j] + numbers[k])
            })
            .collect();
    }
    

    统一更新: 另外,我写了一个基准来表明:

    1. 我的版本并不比@vallentin wrote 慢。差异只有 1-2 微秒。
    2. Itertools 版本不是“慢 100 倍”,而是慢了 4/3 倍。

    基准可用 gist。 这里是结果(itertools - 使用 itertools crate,indexing - 来自 vallentin 和 OP 偏好的版本,以及iterators - 我的迭代器解决方案):

    itertools/100           time:   [12.781 us 12.805 us 12.831 us]
    Found 13 outliers among 100 measurements (13.00%)
      13 (13.00%) low severe
    itertools/200           time:   [48.211 us 48.693 us 49.071 us]
    
    indexing/100            time:   [9.9299 us 9.9378 us 9.9467 us]
    Found 14 outliers among 100 measurements (14.00%)
      1 (1.00%) low mild
      1 (1.00%) high mild
      12 (12.00%) high severe
    indexing/200            time:   [39.582 us 39.654 us 39.720 us]
    Found 2 outliers among 100 measurements (2.00%)
      1 (1.00%) high mild
      1 (1.00%) high severe
    
    iterators/100           time:   [9.7633 us 9.7809 us 9.8010 us]
    Found 1 outliers among 100 measurements (1.00%)
      1 (1.00%) high severe
    iterators/200           time:   [38.732 us 38.785 us 38.840 us]
    Found 5 outliers among 100 measurements (5.00%)
      3 (3.00%) high mild
      2 (2.00%) high severe
    

    【讨论】:

    • 感谢您提供两种替代方式。我认为瓦伦丁的答案更具可读性。我同意这不是惯用的。 IMO 真正惯用的方式是 Itertools::combinations。但是,对于我的用例来说,这大约慢了 100 倍。 (你的答案只比接受的答案慢几个百分点。)
    • 你是如何衡量这个的?我很惊讶 itertools 很慢。你是使用标准还是只使用cargo run --release
    • 我正在使用标准并且我修改了这部分。但是,我不希望将此基准提取为我可以分享的形式。一些可能有助于复制这一点的细节:该函数使用不同的切片重复此 ~1000 次,每个切片大约有 30 个元素。 (ofc 标准会多次运行函数本身。)
    • @Unapiedra 我写了一个基准来证明 itertools 并没有慢 100 倍 :^) 检查答案。
    • 对于其他阅读答案的人:作者选择了一个与我报告的速度不同的函数进行基准测试。他们正在对itertools::tuple_combinations 进行基准测试,而我的代码运行缓慢在itertool::combinations。不幸的是,关于这个问题的所有互动(从第一条评论到我的 Q)都充满了屈尊。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多