Hi, I try to iterate over a single item or an given iterator, depending on input data, but the compiler tells me both iterators have different types. Why isn't std::slice::Iter of type impl Iterator? Is there any trick I can apply to make this code compile?
struct BusinessLogic {
data: Vec<u32>,
}
impl BusinessLogic {
fn get_ids(&self) -> impl Iterator<Item = u32> + '_ {
self.data.iter().map(|x| x + 1)
}
}
fn main() {
let bl = BusinessLogic {
data: vec![1, 2, 3],
};
let iterable = if true {
bl.get_ids()
} else {
[1].iter()
};
for element in iterable {
println!("{}", element);
}
}
Unifying bl.get_ids() and [1].iter() as the same type would require looking at what type is behind the impl Iterator returned by get_ids(). This is exactly what opaque types disallow. When you write impl Iterator all consumers will know that some type implementing Iterator is returned, but by design they won't know which type.
Another thing is that bl.get_ids() and [1].iter() return truly different types even if impl Iterator were to be transparent. bl.get_ids() returns iter::Map<vec::Iter<u32>, {closure}> while [1].iter() returns slice::Iter<u32>. These are different types with potentially (in this case definitively) different sizes, so you can't assign both to the same variable.
You can combine/unify different iterator types using enums or trait objects. An enum that can help is Either from the either crate.
fn main() {
let bl = BusinessLogic {
data: vec![1, 2, 3],
};
let iterable = if true {
Left(bl.get_ids())
} else {
Right(iter::once(1))
};
for element in iterable {
println!("{}", element);
}
}
(playground)
The item type must still match, so I’ve replaced [1].iter() with iter::once(1) that has u32 items, not &u32. You could also use <_>::into_iter([1]) or [1].iter().copied().
Trait objects, using the type dyn Iterator<Item = u32>, need some form of indirection: Either by boxing the iterator
fn main() {
let bl = BusinessLogic {
data: vec![1, 2, 3],
};
let iterable: Box<dyn Iterator<Item = u32>> = if true {
Box::new(bl.get_ids())
} else {
Box::new(iter::once(1))
};
for element in iterable {
println!("{}", element);
}
}
Because impl Iterator in return position means that the function has a single, defined, return type that you don't want or can't name, but it lets the caller know that that type implements the Iterator. That anonymous type however is not std::slice::Iter, and even if it was, the caller is not allowed to know that.
Thank you all very much, it seems obvious now and in theory I should have been able to understand that on my own, but sometimes one needs to be pushed a little bit in the right direction. I don't know yet which solution I'll go with, but at least I understood the problem. Thanks again!
Edit: It is really a long way from a language like Java to fully thinking in the way Rust works. In my head, everything still behaves like a Box<dyn Whatever>.