0

How do I write a lazily evaluated double for loop in a functional style in Rust?

The borrowed value is of type usize, which should be trivially copyable.

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`.

Further Notes

  • I am aware that Itertools::combinations(2) does the job. However, I don't want to use it because (1) I want to know how to do it myself and (2) it might be the reason my code is slow, and I want to eliminate that source. (Update: Itertools::tuple_combinations<(_, _)>() is much, much faster and lets one code this in a functional style.)
  • I also tried collecting it into a container first. (0..len).collect::<Vec<_>>().iter().cloned().map(...)
  • I tried the suggested move but then numbers is also moved and hence not available in the next loop.
  • There is no threading or async happening anywhere in this code example.
  • Shepmaster says in this answer that I cannot make lifetime annotations on closures.
  • The reason I don't write two raw loops with early return is, that if I want to say, run .any() to find if a specific value is present, I'd have to move the two loops into a separate function as I cannot put return true; inside the loop unless it's in a separate function.
Unapiedra
  • 15,037
  • 12
  • 64
  • 93

2 Answers2

2

To work around the issue, you can borrow &numbers up front and just shadow numbers. Then after that you can add move to the second closure.

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();
}
vallentin
  • 23,478
  • 6
  • 59
  • 81
0

I would suggest you to write more idiomatic Rust code with iterators instead of indexing. It is just simpler.

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();
}

If you still want use indexing (be aware that much less effective), this would work:

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();
}

UPD: Also, I wrote a benchmark to show that:

  1. My version is not slower than @vallentin wrote. Difference is in matter of 1-2 microseconds.
  2. Itertools version is not "100X slower" but just 4/3 times slower.

Benchmark available as the gist. Here the results (itertools - using itertools crate, indexing - version from vallentin and OP preference, and iterators - my iterators solution):

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
  • Thanks for providing two alternative ways. I think that vallentin's answer is a lot more readable. I agree that it isn't idiomatic. The truly idiomatic way, IMO, is Itertools::combinations. However that is around 100x slower for my use case. (Your answers are only a few percentage points slower than the accepted answer.) – Unapiedra Dec 11 '20 at 21:31
  • How are you measured this? I am very surprised that itertools is slow. Did you use criterion or just `cargo run --release`? – Angelicos Phosphoros Dec 11 '20 at 22:34
  • I am using criterion and I am _only_ modifying exactly this part. However, I do not wish to extract this benchmark into a form that I can share. Some details that might help to replicate this: The function repeats this ~1000 times with different slices, each slice is around 30 elements. (ofc criterion runs the function itself many more times.) – Unapiedra Dec 11 '20 at 23:28
  • @Unapiedra I wrote a benchmark to show that itertools is not 100 times slower :^) Check answer. – Angelicos Phosphoros Dec 12 '20 at 18:04
  • To others reading the answer: The author chose to benchmark a different function than the one I reported slowness on. They are benchmarking `itertools::tuple_combinations`, whereas my code's slowness was in `itertool::combinations`. Unfortunately, all interaction on this question (from the first comment to my Q) has been steeped in condescension. – Unapiedra Dec 14 '20 at 20:52