5

I am learning Rust and recently went through an exercise where I had to iterate through numbers that could go in either direction. I tried the below with unexpected results.

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Point {
    x: i32,
    y: i32
}

fn test() {
    let p1 = Point { x: 1, y: 8 };
    let p2 = Point { x: 3, y: 6 };

    let all_x = p1.x..=p2.x;
    println!("all_x: {:?}", all_x.clone().collect::<Vec<i32>>());
    let all_y = p1.y..=p2.y;
    println!("all_y: {:?}", all_y.clone().collect::<Vec<i32>>());
    
    let points: Vec<Point> = all_x.zip(all_y).map(|(x, y)| Point { x, y }).collect();

    println!("points: {:?}", points);
}

The output was

all_x: [1, 2, 3]
all_y: []
points: []

After some googling I found an explanation and some old answers which basically amount to use (a..b).rev() as needed.

My question is, how do I do this in a dynamic way? If I use an if...else like so

let all_x = if p1.x < p2.x { (p1.x..=p2.x) } else { (p2.x..=p1.x).rev() };

I get a type error because the else is different than the if

   |
58 |       let all_x = if p1.x < p2.x { (p1.x..=p2.x) }
   |                   -                ------------- expected because of this
   |  _________________|
   | |
59 | |     else { (p2.x..=p1.x).rev() };
   | |____________^^^^^^^^^^^^^^^^^^^_- `if` and `else` have incompatible types
   |              |
   |              expected struct `RangeInclusive`, found struct `Rev`
   |
   = note: expected type `RangeInclusive<_>`
            found struct `Rev<RangeInclusive<_>>`

After trying a bunch of different variations on let all_x: dyn Range<Item = i32>, let all_x: dyn Iterator<Item = i32>, etc, the only way I have managed to do this is by turning them into collections and then back to iterators.

let all_x: Vec<i32>;
if p1.x < p2.x { all_x = (p1.x..=p2.x).collect(); }
else { all_x = (p2.x..=p1.x).rev().collect(); }
let all_x = all_x.into_iter();
println!("all_x: {:?}", all_x.clone().collect::<Vec<i32>>());

let all_y: Vec<i32>;
if p1.y < p2.y { all_y = (p1.y..=p2.y).collect(); }
else { all_y = (p2.y..=p1.y).rev().collect(); }
let all_y = all_y.into_iter();
println!("all_y: {:?}", all_y.clone().collect::<Vec<i32>>());

which provides the desired outcome

all_x: [1, 2, 3]
all_y: [8, 7, 6]
points: [Point { x: 1, y: 8 }, Point { x: 2, y: 7 }, Point { x: 3, y: 6 }]

but is a bit repetitive, inelegant and I'm assuming not very efficient at large numbers. Is there a better way to handle this situation?

NOTE: Sorry for including the Point struct. I could not get my example to work with x1, x2, etc. Probably a different question for a different post lol.

Rob Cannon
  • 384
  • 4
  • 16
  • 2
    Advent of code hehe :) – Netwave Dec 05 '21 at 22:13
  • 1
    @Netwave haha, you got me. I tried to scrub as much of that from my example as possible, but the timing doesn't lie. – Rob Cannon Dec 05 '21 at 22:16
  • Also see https://users.rust-lang.org/t/range-and-reversed-range/17309 for a more compact question example, and a few other solutions not mentioned on this page. And I admit, I also came on this page because of Advent of code :) – hsandt Feb 01 '22 at 18:46

2 Answers2

5

You can dynamically dispatch it. Wrapping them into a Box and returning a dynamic object, an Iterator in this case. For example:

fn maybe_reverse_range(init: usize, end: usize, reverse: bool) -> Box<dyn Iterator<Item=usize>> {
    if reverse {
        Box::new((init..end).rev())
    } else {
        Box::new((init..end))
    }
}

Playground

Netwave
  • 40,134
  • 6
  • 50
  • 93
  • Thanks! I should have paid more attention to `Box` in The Book. I made a version that automatically flipped these based on `init` and `end` for my case, but this was spot on. – Rob Cannon Dec 06 '21 at 04:31
  • To be clear for people like me who got confused on range.rev(): rev() does *not* reverse the range (it doesn't transform init..end into end..init), it just iterates backward. So if init >= end and init..end is empty, then (init..end).rev() still iterates on nothing. You need to manually swap init and end based on their comparison to get the range you want. Then, applying rev() is optional, do it only if you need to iterate backward. – hsandt Jan 27 '22 at 16:10
  • Of course, this example demonstrates dynamic dispatch for an Iterator, so it only makes sense because we rev(). – hsandt Jan 27 '22 at 16:16
5

The enum itertools::Either can be used to solve the incompatible type error in the if/else statement. A function like get_range_iter below using Either can reduce the code repetition.

use itertools::Either;
fn get_range_iter(start: i32, end: i32) -> impl Iterator<Item=i32> {
    if start < end {
        Either::Left(start..=end)
    } else {
        Either::Right((end..=start).rev())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Point {
    x: i32,
    y: i32
}

fn main() {
    let p1 = Point { x: 1, y: 8 };
    let p2 = Point { x: 3, y: 6 };

    let all_x = get_range_iter(p1.x, p2.x);
    let all_y = get_range_iter(p1.y, p2.y);

    println!("all_x: {:?}", all_x.collect::<Vec<_>>());
    println!("all_y: {:?}", all_y.collect::<Vec<_>>());

}

Playground

Joe_Jingyu
  • 1,024
  • 6
  • 9