Conditionally iterate over one of several possible iterators

I am trying to switch behavior based Option

on function entry . The idea is to iterate based on whether a given is present Option

. Here's a minimal if silly example:

use std::iter;

fn main() {
    let x: Option<i64> = None;

    // Repeat x 5 times if present, otherwise count from 1 to 5
    for i in match x {
        None => 1..5,
        Some(x) => iter::repeat(x).take(5),
    } {
        println!("{}", i);
    }
}

      

I am getting the error:

error[E0308]: match arms have incompatible types
  --> src/main.rs:7:14
   |
7  |       for i in match x {
   |  ______________^
8  | |         None => 1..5,
9  | |         Some(x) => iter::repeat(x).take(5),
   | |                    ----------------------- match arm with an incompatible type
10 | |     } {
   | |_____^ expected struct `std::ops::Range`, found struct `std::iter::Take`
   |
   = note: expected type `std::ops::Range<{integer}>`
              found type `std::iter::Take<std::iter::Repeat<i64>>`

      

This makes sense, of course, but I'd really like to select my iterator based on the condition, since the code in the for-loop is non-trivial and copying the whole thing just to change the iterator selection would be pretty ugly and unattainable.

I tried to use as Iterator<Item = i64>

on both shoulders, but it gives me an error about non-standard types because it is a trait object. Is there an easy way to do this?

I could of course use .collect()

, as they return the same type and iterate over that vector. This is a nice quick fix, but seems a little overkill for large lists.

+3


source to share


2 answers


You need to have a link to the trait:

use std::iter;

fn main() {
    let mut a;
    let mut b;

    let x: Option<i64> = None;

    // Repeat x 5 times if present, otherwise count from 1 to 5
    let iter: &mut Iterator<Item = i64> = match x {
        None => {
            a = 1..5;
            &mut a
        }
        Some(x) => {
            b = iter::repeat(x).take(5);
            &mut b
        }
    };

    for i in iter {
        println!("{}", i);
    }
}

      

The main disadvantage of this solution is that you need to allocate stack space for each specific type you have. This also means variables for each type. The good thing is that only the used type needs to be initialized.



The same idea, but requires a heap allocation, is to use boxed objects:

use std::iter;

fn main() {
    let x: Option<i64> = None;

    // Repeat x 5 times if present, otherwise count from 1 to 5
    let iter: Box<Iterator<Item = i64>> = match x {
        None => Box::new(1..5),
        Some(x) => Box::new(iter::repeat(x).take(5)),
    };

    for i in iter {
        println!("{}", i);
    }
}

      

This is useful when you want to return an iterator from a function . Stack space is the only pointer and only the required heap space will be allocated.

+7


source


or the box provides a Either

type. If both halves Either

are iterators, then Either

:

extern crate either;

use either::Either;
use std::iter;

fn main() {
    let x: Option<i64> = None;

    // Repeat x 5 times if present, otherwise count from 1 to 5
    let iter = match x {
        None => Either::Left(1..5),
        Some(x) => Either::Right(iter::repeat(x).take(5)),
    };

    for i in iter {
        println!("{}", i);
    }
}

      

Like the previous answer , this still takes up stack space for every particular type you have. However, you don't need separate variables for each specific value.



This type can also be returned from a function , as opposed to object references. Compared to boxed objects, it will always use a fixed size on the stack, no matter which particular type was chosen.

You will find this type (or semantic equivalent) elsewhere, for example futures::Either

+2


source







All Articles