I am getting a compilation error, which I think I understand, but I don't like the solution in the hint.
A simple function with the two arms of the match returning potentially differently sized opaque objects both implementing Iterator
:
pub fn from_file(&self, filename: &str)
-> impl Iterator<Item=Result<String, TokenizerError>>
{
match fs::File::open(filename) {
Ok(file) => self.happy_path(file),
Err(error) => vec![Err(TokenizerError::from(error))].into_iter()
}
}
fn happy_path<R: io::Read>(&self, reader: R)
-> impl Iterator<Item=Result<String, TokenizerError>>
{
io::BufReader::new(reader).lines()
.map(|res_line| ...)
.flat_map(|res_line| ...)
}
happy_path()
returns a very complextFlatMap<....>
type, which as far as I would like to be concerned is an implementation of Iterator
. To match that, in case File::open()
returns an Err
, I construct another iterator with a single element containing the Err
. This gives compilation error:
= note: expected opaque type `impl Iterator<Item = Result<String, TokenizerError>>`
found struct `std::vec::IntoIter<Result<_, TokenizerError>>`
help: you could change the return type to be a boxed trait object
|
34 - -> impl Iterator<Item=Result<String, TokenizerError>>
34 + -> Box<dyn Iterator<Item=Result<String, TokenizerError>>>
|
help: if you change the return type to expect trait objects, box the returned expressions
|
37 ~ Ok(file) => Box::new(self.happy_path(file)),
38 ~ Err(error) => Box::new(vec![Err(TokenizerError::from(error))].into_iter())
|
My interpretation of the error is that the actual opaque types, returned by the two arms of the match
statement, have different sizes at comp time and therefore must be boxed. The reason I am reluctant to follow the advise in the hint is that from_file()
is a public method in my library and would like to keep the signatures as simple as possible. Is there a way for from_file()
to return impl Iterator<Item=Result<String, TokenizerError>>
, or is Box<dyn Iterator<Item=Result<String, TokenizerError>>>
idiomatic and unavoidable?
If you absolutely do not want to box, you can return an enum that implements Iterator
by delegating to the value of the current variant. For example, if you have this:
enum ExampleIterator {
A(A),
B(B),
}
Where A
and B
are types that implement Iterator
(and have the same Item
types!), then you can do something like this:
impl Iterator for ExampleIterator {
type Item = <A as Iterator>::Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::A(a) => a.next(),
Self::B(b) => b.next(),
}
}
}
Here A
could be your FlatMap
type and B
could be std::vec::IntoIter
.
However, to do this right it's a bit more complex. We should really proxy size_hint
as well, and what about FusedIterator
, etc.?
Well, thankfully, someone already did this. The either
crate contains the eponymous Either
enum. An Either<L, R>
contains either Left(L)
or Right(R)
, and many traits are implemented on this type (provided the L
and R
types also do so in a compatible way), including the Iterator
family of traits.
Using this type, it's as easy as wrapping the two iterators in Either::Left
and Either::Right
:
pub fn from_file(
&self,
filename: &str,
) -> impl Iterator<Item = Result<String, TokenizerError>> {
match fs::File::open(filename) {
Ok(file) => Either::Left(self.happy_path(file)),
Err(error) => Either::Right(vec![Err(TokenizerError::from(error))].into_iter()),
}
}
However, you might consider returning Result<impl Iterator<Item = Result<String, TokenizerError>>, std::io::Error>
instead, which makes the whole problem go away by returning a proper Err
instead of a single-element iterator in the case where an I/O error is encountered. If the caller wants to convert this to an iterator, they can deal with this complexity. (It's also worth pointing out that this approach causes a potential branch on every iteration, unless the optimizer can factor out that branch. This may have performance implications.)