I have a library in which I want to convert a bunch of placeholder panic!()s into Results instead.
One problem I have is that I have a parser object which implements the Iterator trait, but it can return None if one of the nodes requests a controlled exit. When that happens the application can resume iteration on the parser (the progress is stored in the parser context; i.e. the parser is "consumed" when iterated over).
One solution is to return Option<Result<...>> from the Iterator's next(), but this feels a little clunky.
What is an acceptable/idiomatic way to handle iterators that can exit early either as a result of normal program flow or due to error, and where the application may need to act on the specific error?
One solution I'm looking at is is that the parser will store an Option which will be set by the iterator on error. The application can ask the parser if it exited due to an error or not, and if there was an error fetch the error context. I'm sure this will work, but I don't know if this is frowned upon or if there's already another (read: more idiomatic) way to do it.
I won't lie, it does feel kind of clunky... but it is a common way of doing things, as seen in e.g. BufRead::lines.
In that case, of course, we're dealing with errors that are rarely expected to occur. Perhaps this pattern is less appropriate in cases where Erris expected to occur (i.e. as a form of control flow). I am not sure; personally, I would probably still use it, and add comments to explain how Result is being used for control flow.
One solution I’m looking at is is that the parser will store an Option which will be set by the iterator on error. The application can ask the parser if it exited due to an error or not, and if there was an error fetch the error context. I’m sure this will work, but I don’t know if this is frowned upon or if there’s already another (read: more idiomatic) way to do it.
This reminds me of itertools::process_results, which internally works in a kind of similar way, and perhaps may suit your goals. It allows you to treat an iterator of Results as if it were an iterator of values, so that you can easily perform any kind of folding operation on it (e.g. .collect, .any, .sum) and get back a Result.
pub fn process_results<I, F, T, E, R>(iterable: I, processor: F) -> Result<R, E>
where I: IntoIterator<Item = Result<T, E>>,
F: FnOnce(impl Iterator<Item=T>) -> R,
;
For a parser, this sounds pretty good, actually. (nit: Consider a vec of errors, so you can have multiple.) Depending what you're parsing, you might be able to report the error but make a guess at what was meant and usefully continue, so it might ever be better to just return your "best guess" and the error, rather than stop processing things when the error is hit. (You might consider an "error" node in an AST for places where you have no good guess.)